diff --git a/lib/pangea/analytics_misc/construct_list_model.dart b/lib/pangea/analytics_misc/construct_list_model.dart index 4cf0e3d2e..520b1a7b8 100644 --- a/lib/pangea/analytics_misc/construct_list_model.dart +++ b/lib/pangea/analytics_misc/construct_list_model.dart @@ -52,8 +52,9 @@ class ConstructListModel { ConstructListModel({ required List uses, + int offset = 0, }) { - updateConstructs(uses); + updateConstructs(uses, offset); } int get totalLemmas => vocabLemmasList.length + grammarLemmasList.length; @@ -63,13 +64,13 @@ class ConstructListModel { /// 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) { + void updateConstructs(List newUses, int offset) { try { _updateUsesList(newUses); _updateConstructMap(newUses); _updateConstructList(); _updateCategoriesToUses(); - _updateMetrics(); + _updateMetrics(offset); } catch (err, s) { ErrorHandler.logError( e: "Failed to update analytics: $err", @@ -148,7 +149,7 @@ class ConstructListModel { } } - void _updateMetrics() { + void _updateMetrics(int offset) { vocabLemmasList = constructList(type: ConstructTypeEnum.vocab) .map((e) => e.lemma) .toSet() @@ -160,10 +161,11 @@ class ConstructListModel { .toList(); prevXP = totalXP; - totalXP = _constructList.fold( - 0, - (total, construct) => total + construct.points, - ); + totalXP = (_constructList.fold( + 0, + (total, construct) => total + construct.points, + )) + + offset; if (totalXP < 0) { totalXP = 0; diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index b143864e0..bd3a9cf53 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -90,10 +90,16 @@ class GetAnalyticsController extends BaseController { await _pangeaController.putAnalytics.lastUpdatedCompleter.future; await _getConstructs(); - constructListModel.updateConstructs([ - ...(_getConstructsLocal() ?? []), - ..._locallyCachedConstructs, - ]); + + final offset = + _pangeaController.userController.publicProfile?.xpOffset ?? 0; + constructListModel.updateConstructs( + [ + ...(_getConstructsLocal() ?? []), + ..._locallyCachedConstructs, + ], + offset, + ); } catch (err, s) { ErrorHandler.logError( e: err, @@ -125,12 +131,16 @@ class GetAnalyticsController extends BaseController { ) async { if (analyticsUpdate.isLogout) return; final oldLevel = constructListModel.level; - constructListModel.updateConstructs(analyticsUpdate.newConstructs); + + final offset = + _pangeaController.userController.publicProfile?.xpOffset ?? 0; + constructListModel.updateConstructs(analyticsUpdate.newConstructs, offset); if (analyticsUpdate.type == AnalyticsUpdateType.server) { await _getConstructs(forceUpdate: true); } - _updateAnalyticsStream(origin: analyticsUpdate.origin); if (oldLevel < constructListModel.level) _onLevelUp(); + if (oldLevel > constructListModel.level) await _onLevelDown(oldLevel); + _updateAnalyticsStream(origin: analyticsUpdate.origin); } void _updateAnalyticsStream({ @@ -146,6 +156,16 @@ class GetAnalyticsController extends BaseController { setState({'level_up': constructListModel.level}); } + Future _onLevelDown(final prevLevel) async { + final offset = + _calculateMinXpForLevel(prevLevel) - constructListModel.totalXP; + await _pangeaController.userController.addXPOffset(offset); + constructListModel.updateConstructs( + [], + _pangeaController.userController.publicProfile!.xpOffset!, + ); + } + /// 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/common/constants/model_keys.dart b/lib/pangea/common/constants/model_keys.dart index 8052bfa60..5e0a1d53f 100644 --- a/lib/pangea/common/constants/model_keys.dart +++ b/lib/pangea/common/constants/model_keys.dart @@ -155,4 +155,5 @@ class ModelKey { static const String analytics = "analytics"; static const String level = "level"; + static const String xpOffset = "xp_offset"; } diff --git a/lib/pangea/user/controllers/user_controller.dart b/lib/pangea/user/controllers/user_controller.dart index 3bff48bca..17a554c56 100644 --- a/lib/pangea/user/controllers/user_controller.dart +++ b/lib/pangea/user/controllers/user_controller.dart @@ -184,13 +184,23 @@ class UserController extends BaseController { publicProfile!.setLevel(targetLanguage, level); } - await client.setUserProfile( - client.userID!, - PangeaEventTypes.profileAnalytics, - publicProfile!.toJson(), - ); + await _savePublicProfile(); } + Future addXPOffset(int offset) async { + final targetLanguage = _pangeaController.languageController.userL2; + if (targetLanguage == null || publicProfile == null) return; + + publicProfile!.addXPOffset(targetLanguage, offset); + await _savePublicProfile(); + } + + Future _savePublicProfile() async => client.setUserProfile( + client.userID!, + PangeaEventTypes.profileAnalytics, + publicProfile!.toJson(), + ); + /// Returns a boolean value indicating whether a new JWT (JSON Web Token) is needed. bool needNewJWT(String token) => Jwt.isExpired(token); diff --git a/lib/pangea/user/models/profile_model.dart b/lib/pangea/user/models/profile_model.dart index 02c46e673..9906752d3 100644 --- a/lib/pangea/user/models/profile_model.dart +++ b/lib/pangea/user/models/profile_model.dart @@ -27,7 +27,9 @@ class PublicProfileModel { final lang = PangeaLanguage.byLangCode(entry.key); if (lang == null) continue; final level = entry.value[ModelKey.level]; - languageAnalytics[lang] = LanguageAnalyticsProfileEntry(level); + final xpOffset = entry.value[ModelKey.xpOffset] ?? 0; + languageAnalytics[lang] = + LanguageAnalyticsProfileEntry(level, xpOffset); } } @@ -47,7 +49,10 @@ class PublicProfileModel { final analytics = {}; if (languageAnalytics != null && languageAnalytics!.isNotEmpty) { for (final entry in languageAnalytics!.entries) { - analytics[entry.key.langCode] = {ModelKey.level: entry.value.level}; + analytics[entry.key.langCode] = { + ModelKey.level: entry.value.level, + ModelKey.xpOffset: entry.value.xpOffset, + }; } } @@ -61,15 +66,24 @@ class PublicProfileModel { void setLevel(LanguageModel language, int level) { languageAnalytics ??= {}; - languageAnalytics![language] ??= LanguageAnalyticsProfileEntry(0); + languageAnalytics![language] ??= LanguageAnalyticsProfileEntry(0, 0); languageAnalytics![language]!.level = level; } + void addXPOffset(LanguageModel language, int xpOffset) { + languageAnalytics ??= {}; + languageAnalytics![language] ??= LanguageAnalyticsProfileEntry(0, 0); + languageAnalytics![language]!.xpOffset += xpOffset; + } + int? get level => languageAnalytics?[targetLanguage]?.level; + + int? get xpOffset => languageAnalytics?[targetLanguage]?.xpOffset; } class LanguageAnalyticsProfileEntry { int level; + int xpOffset = 0; - LanguageAnalyticsProfileEntry(this.level); + LanguageAnalyticsProfileEntry(this.level, this.xpOffset); }