diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index a22d826cd..f89492aca 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -25,6 +25,27 @@ class GetAnalyticsController { CachedStreamController analyticsStream = CachedStreamController(); + ConstructListModel vocabModel = ConstructListModel( + type: ConstructTypeEnum.vocab, + uses: [], + ); + ConstructListModel grammarModel = ConstructListModel( + type: ConstructTypeEnum.morph, + uses: [], + ); + + List get allConstructUses { + final List storedUses = getConstructsLocal() ?? []; + final List localUses = locallyCachedConstructs; + + final List allConstructs = [ + ...storedUses, + ...localUses, + ]; + + return allConstructs; + } + /// The previous XP points of the user, before the last update. /// Used for animating analytics updates. int? prevXP; @@ -73,7 +94,11 @@ class GetAnalyticsController { .listen(onAnalyticsUpdate); _pangeaController.putAnalytics.lastUpdatedCompleter.future.then((_) { - getConstructs().then((_) => updateAnalyticsStream()); + getConstructs().then((_) { + vocabModel.updateConstructs(allConstructUses); + grammarModel.updateConstructs(allConstructUses); + updateAnalyticsStream(); + }); }); } @@ -87,6 +112,8 @@ class GetAnalyticsController { } Future onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async { + vocabModel.updateConstructs(analyticsUpdate.newConstructs); + grammarModel.updateConstructs(analyticsUpdate.newConstructs); if (analyticsUpdate.isLogout) return; if (analyticsUpdate.type == AnalyticsUpdateType.server) { await getConstructs(forceUpdate: true); @@ -134,18 +161,6 @@ class GetAnalyticsController { return words.points + morphs.points; } - List get allConstructUses { - final List storedUses = getConstructsLocal() ?? []; - final List localUses = locallyCachedConstructs; - - final List allConstructs = [ - ...storedUses, - ...localUses, - ]; - - return allConstructs; - } - /// A local cache of eventIds and construct uses for messages sent since the last update. /// It's a map of eventIDs to a list of OneConstructUses. Not just a list of OneConstructUses /// because, with practice activity constructs, we might need to add to the list for a given diff --git a/lib/pangea/controllers/put_analytics_controller.dart b/lib/pangea/controllers/put_analytics_controller.dart index f887a6d66..ba02456d3 100644 --- a/lib/pangea/controllers/put_analytics_controller.dart +++ b/lib/pangea/controllers/put_analytics_controller.dart @@ -138,7 +138,11 @@ class PutAnalyticsController extends BaseController { _addLocalMessage(eventID, constructs).then( (_) { _clearDraftUses(roomID); - _decideWhetherToUpdateAnalyticsRoom(level, data.origin); + _decideWhetherToUpdateAnalyticsRoom( + level, + data.origin, + data.constructs, + ); }, ); } @@ -202,8 +206,12 @@ class PutAnalyticsController extends BaseController { } final level = _pangeaController.getAnalytics.level; + + // the list 'uses' gets altered in the _addLocalMessage method, + // so copy it here to that the list of new uses is accurate + final List newUses = List.from(uses); _addLocalMessage('draft$roomID', uses).then( - (_) => _decideWhetherToUpdateAnalyticsRoom(level, origin), + (_) => _decideWhetherToUpdateAnalyticsRoom(level, origin, newUses), ); } @@ -246,6 +254,7 @@ class PutAnalyticsController extends BaseController { void _decideWhetherToUpdateAnalyticsRoom( int prevLevel, AnalyticsUpdateOrigin? origin, + List newConstructs, ) { // cancel the last timer that was set on message event and // reset it to fire after _minutesBeforeUpdate minutes @@ -266,7 +275,11 @@ class PutAnalyticsController extends BaseController { newLevel > prevLevel ? sendLocalAnalyticsToAnalyticsRoom() : analyticsUpdateStream.add( - AnalyticsUpdate(AnalyticsUpdateType.local, origin: origin), + AnalyticsUpdate( + AnalyticsUpdateType.local, + newConstructs, + origin: origin, + ), ); } @@ -334,6 +347,7 @@ class PutAnalyticsController extends BaseController { analyticsUpdateStream.add( AnalyticsUpdate( AnalyticsUpdateType.server, + [], isLogout: onLogout, ), ); @@ -397,7 +411,13 @@ enum AnalyticsUpdateOrigin { class AnalyticsUpdate { final AnalyticsUpdateType type; final AnalyticsUpdateOrigin? origin; + final List newConstructs; final bool isLogout; - AnalyticsUpdate(this.type, {this.isLogout = false, this.origin}); + AnalyticsUpdate( + this.type, + this.newConstructs, { + this.isLogout = false, + this.origin, + }); } diff --git a/lib/pangea/models/analytics/construct_list_model.dart b/lib/pangea/models/analytics/construct_list_model.dart index 23763824c..4a0d7c83a 100644 --- a/lib/pangea/models/analytics/construct_list_model.dart +++ b/lib/pangea/models/analytics/construct_list_model.dart @@ -1,138 +1,88 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; /// A wrapper around a list of [OneConstructUse]s, used to simplify /// the process of filtering / sorting / displaying the events. /// Takes a construct type and a list of events class ConstructListModel { final ConstructTypeEnum? type; - final List _uses; - List? _constructList; - List? _typedConstructs; /// A map of lemmas to ConstructUses, each of which contains a lemma /// key = lemmma + constructType.string, value = ConstructUses - Map? _constructMap; + final Map _constructMap = {}; + + /// Storing this to avoid re-running the sort operation each time this needs to + /// be accessed. It contains the same information as _constructMap, but sorted. + List constructList = []; ConstructListModel({ required this.type, required List uses, - }) : _uses = uses; - - List get uses => - _uses.where((use) => use.constructType == type || type == null).toList(); - - /// All unique lemmas used in the construct events - List get lemmas => constructList.map((e) => e.lemma).toSet().toList(); + }) { + updateConstructs(uses); + } - /// All unique lemmas used in the construct events with non-zero points - List get lemmasWithPoints => - constructListWithPoints.map((e) => e.lemma).toSet().toList(); + /// Given a list of new construct uses, update the map of construct + /// IDs to ConstructUses and re-sort the list of ConstructUses + void updateConstructs(List newUses) { + final List filteredUses = newUses + .where((use) => use.constructType == type || type == null) + .toList(); + _updateConstructMap(filteredUses); + _updateConstructList(); + } /// A map of lemmas to ConstructUses, each of which contains a lemma /// key = lemmma + constructType.string, value = ConstructUses - void _buildConstructMap() { - final Map> lemmaToUses = {}; - for (final use in uses) { + void _updateConstructMap(final List newUses) { + for (final use in newUses) { if (use.lemma == null) continue; - lemmaToUses[use.lemma! + use.constructType.string] ??= []; - lemmaToUses[use.lemma! + use.constructType.string]!.add(use); + final currentUses = _constructMap[use.identifier.string] ?? + ConstructUses( + uses: [], + constructType: use.constructType, + lemma: use.lemma!, + ); + currentUses.uses.add(use); + _constructMap[use.identifier.string] = currentUses; } - - _constructMap = lemmaToUses.map( - (key, value) => MapEntry( - key, - ConstructUses( - uses: value, - constructType: value.first.constructType, - lemma: value.first.lemma!, - ), - ), - ); - } - - ConstructUses? getConstructUses(String lemma, ConstructTypeEnum type) { - if (_constructMap == null) _buildConstructMap(); - return _constructMap![lemma + type.string]; } /// A list of ConstructUses, each of which contains a lemma and /// a list of uses, sorted by the number of uses - List get constructList { - // the list of uses doesn't change so we don't have to re-calculate this - if (_constructList != null) return _constructList!; - - if (_constructMap == null) _buildConstructMap(); - - _constructList = _constructMap!.values.toList(); - - _constructList!.sort((a, b) { + void _updateConstructList() { + // TODO check how expensive this is + constructList = _constructMap.values.toList(); + constructList.sort((a, b) { final comp = b.uses.length.compareTo(a.uses.length); if (comp != 0) return comp; return a.lemma.compareTo(b.lemma); }); + } - return _constructList!; + ConstructUses? getConstructUses(ConstructIdentifier identifier) { + return _constructMap[identifier.string]; } List get constructListWithPoints => constructList.where((constructUse) => constructUse.points > 0).toList(); - get maxXPPerLemma { - return type != null - ? type!.maxXPPerLemma - : ConstructTypeEnum.vocab.maxXPPerLemma; - } + /// All unique lemmas used in the construct events with non-zero points + List get lemmasWithPoints => + constructListWithPoints.map((e) => e.lemma).toSet().toList(); - /// A list of ConstructUseTypeUses, each of which - /// contains a lemma, a use type, and a list of uses - List get typedConstructs { - if (_typedConstructs != null) return _typedConstructs!; - final List typedConstructs = []; - for (final construct in constructList) { - final typeToUses = >{}; - for (final use in construct.uses) { - typeToUses[use.useType] ??= []; - typeToUses[use.useType]!.add(use); - } - for (final typeEntry in typeToUses.entries) { - typedConstructs.add( - ConstructUseTypeUses( - lemma: construct.lemma, - constructType: typeEntry.value.first.constructType, - useType: typeEntry.key, - uses: typeEntry.value, - ), - ); - } - } - return typedConstructs; - } + int get maxXPPerLemma => + type?.maxXPPerLemma ?? ConstructTypeEnum.vocab.maxXPPerLemma; /// The total number of points for all uses of this construct type int get points { - // double totalPoints = 0; - return typedConstructs.fold( - 0, - (total, typedConstruct) => - total + - typedConstruct.useType.pointValue * typedConstruct.uses.length, - ); - // Commenting this out for now - // Minimize the amount of points given for repeated uses of the same lemma. - // i.e., if a lemma is used 4 times without assistance, the point value for - // a use without assistance is 3. So the points would be - // 3/1 + 3/2 + 3/3 + 3/4 = 3 + 1.5 + 1 + 0.75 = 5.25 (instead of 12) - // for (final typedConstruct in typedConstructs) { - // final pointValue = typedConstruct.useType.pointValue; - // double calc = 0.0; - // for (int k = 1; k <= typedConstruct.uses.length; k++) { - // calc += pointValue / k; - // } - // totalPoints += calc; - // } - // return totalPoints.round(); + int totalPoints = 0; + for (final constructUse in _constructMap.values.toList()) { + totalPoints += constructUse.points; + } + return totalPoints; } } @@ -166,19 +116,3 @@ class ConstructUses { return _lastUsed = lastUse; } } - -/// One lemma, a use type, and a list of uses -/// for that lemma and use type -class ConstructUseTypeUses { - final ConstructUseTypeEnum useType; - final ConstructTypeEnum constructType; - final String lemma; - final List uses; - - ConstructUseTypeUses({ - required this.useType, - required this.constructType, - required this.lemma, - required this.uses, - }); -} diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index c472752b5..1a5d400c7 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -133,6 +134,11 @@ class OneConstructUse { } int get pointValue => useType.pointValue; + + ConstructIdentifier get identifier => ConstructIdentifier( + lemma: lemma!, + type: constructType, + ); } class ConstructUseMetaData { diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 65f02c4f7..db8e4a023 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -51,6 +51,8 @@ class ConstructIdentifier { int get hashCode { return lemma.hashCode ^ type.hashCode; } + + String get string => "$lemma-${type.string}"; } class CandidateMessage { diff --git a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart index a9798d1ca..cf42f4e4a 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart'; import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; @@ -38,13 +37,6 @@ class LearningProgressIndicatorsState /// A stream subscription to listen for updates to /// the analytics data, either locally or from events StreamSubscription? _analyticsUpdateSubscription; - - /// Vocabulary constructs model - ConstructListModel? words; - - /// Morph constructs model - ConstructListModel? morphs; - bool loading = true; // Some buggy stuff is happening with this data not being updated at login, so switching @@ -81,15 +73,6 @@ class LearningProgressIndicatorsState /// Update the analytics data shown in the UI. This comes from a /// combination of stored events and locally cached data. Future updateAnalyticsData(List constructs) async { - words = ConstructListModel( - type: ConstructTypeEnum.vocab, - uses: constructs, - ); - morphs = ConstructListModel( - type: ConstructTypeEnum.morph, - uses: constructs, - ); - currentConstructs = constructs; if (loading) loading = false; if (mounted) setState(() {}); @@ -99,9 +82,9 @@ class LearningProgressIndicatorsState ConstructListModel? getConstructsModel(ProgressIndicatorEnum indicator) { switch (indicator) { case ProgressIndicatorEnum.wordsUsed: - return words; + return _pangeaController.getAnalytics.vocabModel; case ProgressIndicatorEnum.morphsUsed: - return morphs; + return _pangeaController.getAnalytics.grammarModel; default: return null; } @@ -111,9 +94,11 @@ class LearningProgressIndicatorsState int? getProgressPoints(ProgressIndicatorEnum indicator) { switch (indicator) { case ProgressIndicatorEnum.wordsUsed: - return words?.lemmasWithPoints.length; + return _pangeaController + .getAnalytics.vocabModel.lemmasWithPoints.length; case ProgressIndicatorEnum.morphsUsed: - return morphs?.lemmasWithPoints.length; + return _pangeaController + .getAnalytics.grammarModel.lemmasWithPoints.length; case ProgressIndicatorEnum.level: return level; } diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart index c248c0dcd..c659490b3 100644 --- a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -72,8 +73,10 @@ class TargetTokensController { for (final construct in token.constructs) { final constructUseModel = constructList.getConstructUses( - construct.id.lemma, - construct.id.type, + ConstructIdentifier( + lemma: construct.id.lemma, + type: construct.id.type, + ), ); if (constructUseModel != null) { construct.xp += constructUseModel.points;