diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f48cdd233..755f8bbf2 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4827,5 +4827,6 @@ "startChatting": "Start chatting", "referFriends": "Refer friends", "referFriendDialogTitle": "Invite a friend to your conversation", - "referFriendDialogDesc": "Do you have a friend who is excited to learn a new language with you? Then copy and send this invitation link to join and start chatting with you today." + "referFriendDialogDesc": "Do you have a friend who is excited to learn a new language with you? Then copy and send this invitation link to join and start chatting with you today.", + "youUnlocked": "You've unlocked" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index c72b92a63..025de396d 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -30,6 +30,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; +import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart'; import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; @@ -365,7 +366,9 @@ class ChatController extends State _levelSubscription = pangeaController.getAnalytics.stateStream .where( - (update) => update is Map && update['level_up'] != null, + (update) => + update is Map && + (update['level_up'] != null || update['unlocked_constructs'] != null), ) // .listen( // (update) => Future.delayed( @@ -380,13 +383,20 @@ class ChatController extends State // remove delay now that GetAnalyticsController._onLevelUp // is async is should take roughly 500ms to make requests anyway (update) { - LevelUpUtil.showLevelUpDialog( - update['level_up'], - update['analytics_room_id'], - update["construct_summary_state_event_id"], - update['construct_summary'], - context, - ); + if (update['level_up'] != null) { + LevelUpUtil.showLevelUpDialog( + update['level_up'], + update['analytics_room_id'], + update["construct_summary_state_event_id"], + update['construct_summary'], + context, + ); + } else if (update['unlocked_constructs'] != null) { + showUnlockedMorphsSnackbar( + update['unlocked_constructs'], + context, + ); + } }, ); // Pangea# diff --git a/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart b/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart index 9f5cc1d3d..67d140b1d 100644 --- a/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart +++ b/lib/pangea/analytics_details_popup/morph_analytics_list_view.dart @@ -158,7 +158,7 @@ class MorphFeatureBox extends StatelessWidget { morphFeature: morphFeature, morphTag: morphTag, constructAnalytics: analytics, - onTap: analytics.points > 0 + onTap: analytics.points > 10 ? () => onConstructZoom(id) : null, ); @@ -196,16 +196,17 @@ class MorphTagChip extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final unlocked = constructAnalytics.points > 10; return InkWell( borderRadius: BorderRadius.circular(32.0), onTap: onTap, child: Opacity( - opacity: constructAnalytics.points > 0 ? 1.0 : 0.3, + opacity: unlocked ? 1.0 : 0.3, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(32.0), - gradient: constructAnalytics.points > 0 + gradient: unlocked ? LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, @@ -215,7 +216,7 @@ class MorphTagChip extends StatelessWidget { ], ) : null, - color: constructAnalytics.points > 0 ? null : theme.disabledColor, + color: unlocked ? null : theme.disabledColor, ), padding: const EdgeInsets.symmetric( vertical: 4.0, @@ -228,8 +229,7 @@ class MorphTagChip extends StatelessWidget { SizedBox( width: 28.0, height: 28.0, - child: constructAnalytics.points > 0 || - Matrix.of(context).client.isSupportAccount + child: unlocked || Matrix.of(context).client.isSupportAccount ? MorphIcon( morphFeature: morphFeature, morphTag: morphTag, diff --git a/lib/pangea/analytics_misc/construct_list_model.dart b/lib/pangea/analytics_misc/construct_list_model.dart index 28e17fc2c..4022b6b17 100644 --- a/lib/pangea/analytics_misc/construct_list_model.dart +++ b/lib/pangea/analytics_misc/construct_list_model.dart @@ -36,6 +36,22 @@ class ConstructListModel { /// A list of unique grammar lemmas List grammarLemmasList = []; + List get unlockedGrammarLemmas { + final morphs = constructList(type: ConstructTypeEnum.morph); + final List unlocked = []; + for (final morph in grammarLemmasList) { + final matches = morphs.where((m) => m.lemma == morph); + final totalPoints = matches.fold( + 0, + (total, match) => total + match.points, + ); + if (totalPoints > 10) { + unlocked.add(matches.first.id); + } + } + return unlocked; + } + /// Analytics data consumed by widgets. Updated each time new analytics come in. int prevXP = 0; int totalXP = 0; diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 7d8f3229f..727547f9e 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/common/constants/local.key.dart'; import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; @@ -139,7 +140,12 @@ class GetAnalyticsController extends BaseController { final offset = _pangeaController.userController.publicProfile?.xpOffset ?? 0; + final prevUnlockedMorphs = constructListModel.unlockedGrammarLemmas.toSet(); constructListModel.updateConstructs(analyticsUpdate.newConstructs, offset); + final newUnlockedMorphs = constructListModel.unlockedGrammarLemmas + .toSet() + .difference(prevUnlockedMorphs); + if (analyticsUpdate.type == AnalyticsUpdateType.server) { await _getConstructs(forceUpdate: true); } @@ -149,6 +155,9 @@ class GetAnalyticsController extends BaseController { if (oldLevel > constructListModel.level) { await _onLevelDown(constructListModel.level, oldLevel); } + if (newUnlockedMorphs.isNotEmpty) { + _onUnlockMorphLemmas(newUnlockedMorphs); + } _updateAnalyticsStream(origin: analyticsUpdate.origin); // Update public profile each time that new analytics are added. // If the level hasn't changed, this will not send an update to the server. @@ -187,6 +196,10 @@ class GetAnalyticsController extends BaseController { ); } + void _onUnlockMorphLemmas(Set unlocked) { + setState({'unlocked_constructs': unlocked}); + } + /// 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/analytics_summary/learning_progress_indicators.dart b/lib/pangea/analytics_summary/learning_progress_indicators.dart index ac28bc546..0032513f2 100644 --- a/lib/pangea/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/analytics_summary/learning_progress_indicators.dart @@ -85,7 +85,7 @@ class LearningProgressIndicatorsState int uniqueLemmas(ProgressIndicatorEnum indicator) { switch (indicator) { case ProgressIndicatorEnum.morphsUsed: - return _constructsModel.grammarLemmas; + return _constructsModel.unlockedGrammarLemmas.length; case ProgressIndicatorEnum.wordsUsed: return _constructsModel.vocabLemmas; default: diff --git a/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart b/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart new file mode 100644 index 000000000..48f7e7ba2 --- /dev/null +++ b/lib/pangea/chat/utils/unlocked_morphs_snackbar.dart @@ -0,0 +1,77 @@ +// ignore_for_file: depend_on_referenced_packages, implementation_imports + +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; +import 'package:fluffychat/pangea/morphs/morph_icon.dart'; + +void showUnlockedMorphsSnackbar( + Set unlockedConstructs, + BuildContext context, +) { + for (final construct in unlockedConstructs) { + final copy = getGrammarCopy( + category: construct.category, + lemma: construct.lemma, + context: context, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + behavior: FluffyThemes.isColumnMode(context) + ? SnackBarBehavior.floating + : SnackBarBehavior.fixed, + width: FluffyThemes.isColumnMode(context) + ? MediaQuery.of(context).size.width + : null, + showCloseIcon: true, + duration: const Duration(seconds: 5), + dismissDirection: DismissDirection.none, + backgroundColor: Theme.of(context).colorScheme.surface, + content: Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 16.0, + alignment: WrapAlignment.center, + children: [ + Text( + L10n.of(context).youUnlocked, + style: TextStyle( + fontSize: FluffyThemes.isColumnMode(context) ? 32.0 : 16.0, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + spacing: 16.0, + children: [ + Flexible( + child: Text( + copy ?? construct.lemma, + style: TextStyle( + fontSize: + FluffyThemes.isColumnMode(context) ? 32.0 : 16.0, + color: AppConfig.gold, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + MorphIcon( + morphFeature: construct.category, + morphTag: construct.lemma, + ), + ], + ), + ], + ), + ), + ), + ); + } +}