From e2ca788f81dd9991831959336bab8cecd26895b8 Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:03:21 -0500 Subject: [PATCH] Add message meaning challenge activity (#1706) * Add message meaning mode to toolbar --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com> Co-authored-by: ggurdin --- assets/l10n/intl_en.arb | 7 +- .../construct_use_type_enum.dart | 27 ++++++++ .../message_analytics_controller.dart | 22 +++---- .../event_wrappers/pangea_message_event.dart | 13 ++-- .../events/models/pangea_token_model.dart | 42 +++++++++++- .../enums/language_level_type_enum.dart | 3 +- .../toolbar/enums/activity_type_enum.dart | 19 +++++- .../toolbar/enums/message_mode_enum.dart | 25 +++----- .../models/message_activity_request.dart | 5 ++ .../models/practice_activity_model.dart | 1 + .../practice_activity_record_model.dart | 13 ++++ lib/pangea/toolbar/repo/practice_repo.dart | 2 + ...eaning_static_practice_activity_model.dart | 59 +++++++++++++++++ .../widgets/message_meaning_button.dart | 43 +++++++++++++ .../toolbar/widgets/message_meaning_card.dart | 41 ++++++++++++ .../widgets/message_mode_locked_card.dart | 5 +- .../widgets/message_selection_overlay.dart | 24 ++++++- .../toolbar/widgets/message_token_text.dart | 6 +- .../toolbar/widgets/message_toolbar.dart | 11 +++- .../multiple_choice_activity.dart | 4 +- .../practice_activity_card.dart | 8 ++- .../toolbar/widgets/toolbar_button.dart | 42 ++++++------ .../toolbar_button_and_progress_row.dart | 14 ++-- pubspec.lock | 64 +++++++++++++++++++ 24 files changed, 421 insertions(+), 79 deletions(-) create mode 100644 lib/pangea/toolbar/repo/word_meaning_static_practice_activity_model.dart create mode 100644 lib/pangea/toolbar/widgets/message_meaning_button.dart create mode 100644 lib/pangea/toolbar/widgets/message_meaning_card.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 8210c1949..595b3ff11 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4815,5 +4815,10 @@ "open": "Open", "waitingForServer": "Waiting for server...", "appIntroduction": "FluffyChat lets you chat with your friends across different messengers. Learn more at https://matrix.org or just tap *Continue*.", - "whatIsLemma": "What is the lemma?" + "whatIsLemma": "What is the lemma?", + "constructUseCorMmDesc": "Correct message meaning", + "constructUseIncMmDesc": "Incorrect message meaning", + "constructUseIgnMmDesc": "Ignored message meaning", + "clickForMeaningActivity": "Click here for a Meaning Challenge", + "meaning": "Meaning" } diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index f6665638b..3100140c9 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -60,6 +60,11 @@ enum ConstructUseTypeEnum { /// User can select any emoji em, + /// message meaning activity + corMM, + incMM, + ignMM, + /// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client nan } @@ -121,6 +126,12 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return L10n.of(context).constructUseEmojiDesc; case ConstructUseTypeEnum.pvm: return L10n.of(context).constructUsePvmDesc; + case ConstructUseTypeEnum.corMM: + return L10n.of(context).constructUseCorMmDesc; + case ConstructUseTypeEnum.incMM: + return L10n.of(context).constructUseIncMmDesc; + case ConstructUseTypeEnum.ignMM: + return L10n.of(context).constructUseIgnMmDesc; case ConstructUseTypeEnum.nan: return L10n.of(context).constructUseNanDesc; } @@ -161,6 +172,10 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return ActivityTypeEnum.morphId.icon; case ConstructUseTypeEnum.em: return ActivityTypeEnum.emoji.icon; + case ConstructUseTypeEnum.corMM: + case ConstructUseTypeEnum.incMM: + case ConstructUseTypeEnum.ignMM: + return ActivityTypeEnum.messageMeaning.icon; case ConstructUseTypeEnum.pvm: return Icons.mic; case ConstructUseTypeEnum.unk: @@ -195,6 +210,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corIt: case ConstructUseTypeEnum.em: + case ConstructUseTypeEnum.corMM: return 1; case ConstructUseTypeEnum.ignIt: @@ -204,11 +220,13 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ignHWL: case ConstructUseTypeEnum.ignL: case ConstructUseTypeEnum.ignM: + case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.nan: return 0; case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.incMM: return -1; case ConstructUseTypeEnum.incIt: @@ -253,6 +271,9 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corM: case ConstructUseTypeEnum.incM: case ConstructUseTypeEnum.ignM: + case ConstructUseTypeEnum.corMM: + case ConstructUseTypeEnum.incMM: + case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.em: case ConstructUseTypeEnum.nan: return false; @@ -288,6 +309,9 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corM: case ConstructUseTypeEnum.ignM: case ConstructUseTypeEnum.incM: + case ConstructUseTypeEnum.corMM: + case ConstructUseTypeEnum.incMM: + case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.em: return LearningSkillsEnum.reading; case ConstructUseTypeEnum.pvm: @@ -313,6 +337,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corL: case ConstructUseTypeEnum.corM: case ConstructUseTypeEnum.em: + case ConstructUseTypeEnum.corMM: return AnalyticsSummaryEnum.numChoicesCorrect; case ConstructUseTypeEnum.incIt: @@ -322,6 +347,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incHWL: case ConstructUseTypeEnum.incL: case ConstructUseTypeEnum.incM: + case ConstructUseTypeEnum.incMM: return AnalyticsSummaryEnum.numChoicesIncorrect; case ConstructUseTypeEnum.ignIt: @@ -331,6 +357,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ignHWL: case ConstructUseTypeEnum.ignL: case ConstructUseTypeEnum.ignM: + case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.nan: return null; } diff --git a/lib/pangea/analytics_misc/message_analytics_controller.dart b/lib/pangea/analytics_misc/message_analytics_controller.dart index 2e17eb15e..ffb6f4e2f 100644 --- a/lib/pangea/analytics_misc/message_analytics_controller.dart +++ b/lib/pangea/analytics_misc/message_analytics_controller.dart @@ -104,9 +104,7 @@ class MessageAnalyticsEntry { } void _popQueue() { - if (hasHiddenWordActivity) { - _activityQueue.removeAt(0); - } + if (_activityQueue.isNotEmpty) _activityQueue.removeAt(0); } void _filterQueue(ActivityTypeEnum activityType) { @@ -123,6 +121,9 @@ class MessageAnalyticsEntry { bool get hasHiddenWordActivity => nextActivity?.activityType.hiddenType ?? false; + bool get hasMessageMeaningActivity => _activityQueue + .any((a) => a.activityType == ActivityTypeEnum.messageMeaning); + int get numActivities => _activityQueue.length; // /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening @@ -141,15 +142,12 @@ class MessageAnalyticsEntry { } } - /// Adds a word focus listening activity to the front of the queue + /// Add a message meaning activity to the front of the queue /// And limits to _maxQueueLength activities - void addTokenToActivityQueue( - PangeaToken token, { - ActivityTypeEnum type = ActivityTypeEnum.wordMeaning, - }) { + void addMessageMeaningActivity() { final entry = TargetTokensAndActivityType( - tokens: [token], - activityType: ActivityTypeEnum.wordMeaning, + tokens: _tokens, + activityType: ActivityTypeEnum.messageMeaning, ); _pushQueue(entry); } @@ -204,9 +202,7 @@ class MessageAnalyticsEntry { ); } - void onActivityComplete() { - _popQueue(); - } + void onActivityComplete() => _popQueue(); void exitPracticeFlow() => _clearQueue(); diff --git a/lib/pangea/events/event_wrappers/pangea_message_event.dart b/lib/pangea/events/event_wrappers/pangea_message_event.dart index 971a4f77c..fbcbb5508 100644 --- a/lib/pangea/events/event_wrappers/pangea_message_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_message_event.dart @@ -461,11 +461,11 @@ class PangeaMessageEvent { RepresentationEvent? representationByLanguage(String langCode) => representations.firstWhereOrNull( - (element) => element.langCode == langCode, + (element) => element.langCode.split("-")[0] == langCode.split("-")[0], ); int translationIndex(String langCode) => representations.indexWhere( - (element) => element.langCode == langCode, + (element) => element.langCode.split("-")[0] == langCode.split("-")[0], ); String translationTextSafe(String langCode) { @@ -596,7 +596,8 @@ class PangeaMessageEvent { /// Should almost always be true. Useful in the case that the message /// display rep has the langCode "unk" - bool get messageDisplayLangIsL2 => messageDisplayLangCode == l2Code; + bool get messageDisplayLangIsL2 => + messageDisplayLangCode.split("-")[0] == l2Code?.split("-")[0]; String get messageDisplayLangCode { final bool immersionMode = MatrixState @@ -684,7 +685,11 @@ class PangeaMessageEvent { bool debug = false, }) => _practiceActivityEvents - .where((event) => event.practiceActivity.langCode == langCode) + .where( + (event) => + event.practiceActivity.langCode.split("-")[0] == + langCode.split("")[0], + ) .toList(); /// Returns a list of [PracticeActivityEvent] for the user's active l2. diff --git a/lib/pangea/events/models/pangea_token_model.dart b/lib/pangea/events/models/pangea_token_model.dart index 7a676a56d..bd41deb8e 100644 --- a/lib/pangea/events/models/pangea_token_model.dart +++ b/lib/pangea/events/models/pangea_token_model.dart @@ -213,6 +213,30 @@ class PangeaToken { ); } + List allUses( + ConstructUseTypeEnum type, + ConstructUseMetaData metadata, + ) { + final List uses = []; + if (!lemma.saveVocab) return uses; + + uses.add(toVocabUse(type, metadata)); + for (final morphFeature in morph.keys) { + uses.add( + OneConstructUse( + useType: type, + lemma: morph[morphFeature], + form: text.content, + constructType: ConstructTypeEnum.morph, + metadata: metadata, + category: morphFeature, + ), + ); + } + + return uses; + } + bool isActivityBasicallyEligible( ActivityTypeEnum a, [ String? morphFeature, @@ -238,6 +262,7 @@ class PangeaToken { return lemma.saveVocab && text.content.toLowerCase() != lemma.text.toLowerCase(); case ActivityTypeEnum.emoji: + case ActivityTypeEnum.messageMeaning: return true; case ActivityTypeEnum.morphId: return morph.isNotEmpty && canGenerate; @@ -299,6 +324,7 @@ class PangeaToken { .any((u) => u == a.correctUse); // Note that it matters less if they did morphId in general, than if they did it with the particular feature case ActivityTypeEnum.morphId: + // TODO: investigate if we take out condition "|| morphTag == null", will we get the expected number of morph activities? if (morphFeature == null || morphTag == null) { debugger(when: kDebugMode); return false; @@ -306,6 +332,13 @@ class PangeaToken { return morphConstruct(morphFeature, morphTag) .uses .any((u) => u.useType == a.correctUse && u.form == text.content); + case ActivityTypeEnum.messageMeaning: + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "should not call didActivitySuccessfully for ActivityTypeEnum.messageMeaning", + data: toJson(), + ); + return true; } } @@ -316,14 +349,15 @@ class PangeaToken { ]) { switch (a) { case ActivityTypeEnum.wordMeaning: - if (daysSinceLastUseByType(ActivityTypeEnum.wordMeaning) < 7) { + if (daysSinceLastUseByType(ActivityTypeEnum.wordMeaning) < 7 || + daysSinceLastUseByType(ActivityTypeEnum.messageMeaning) < 7) { return false; } if (isContentWord) { - return vocabConstruct.points < 3; + return vocabConstruct.points < 1; } else if (canBeDefined) { - return vocabConstruct.points < 2; + return vocabConstruct.points < 1; } else { return false; } @@ -342,6 +376,7 @@ class PangeaToken { // return _didActivitySuccessfully(ActivityTypeEnum.wordMeaning) && // daysSinceLastUseByType(a) > 7; case ActivityTypeEnum.emoji: + case ActivityTypeEnum.messageMeaning: return true; case ActivityTypeEnum.morphId: if (morphFeature == null || morphTag == null) { @@ -407,6 +442,7 @@ class PangeaToken { case ActivityTypeEnum.emoji: case ActivityTypeEnum.wordFocusListening: case ActivityTypeEnum.hiddenWordListening: + case ActivityTypeEnum.messageMeaning: return true; } } diff --git a/lib/pangea/learning_settings/enums/language_level_type_enum.dart b/lib/pangea/learning_settings/enums/language_level_type_enum.dart index 505e05ada..148664d92 100644 --- a/lib/pangea/learning_settings/enums/language_level_type_enum.dart +++ b/lib/pangea/learning_settings/enums/language_level_type_enum.dart @@ -5,7 +5,7 @@ extension LanguageLevelTypeEnumExtension on LanguageLevelTypeEnum { String get string { switch (this) { case LanguageLevelTypeEnum.preA1: - return 'Pre-A1'; + return 'PREA1'; case LanguageLevelTypeEnum.a1: return 'A1'; case LanguageLevelTypeEnum.a2: @@ -65,6 +65,7 @@ extension LanguageLevelTypeEnumExtension on LanguageLevelTypeEnum { static LanguageLevelTypeEnum fromString(String? value) { switch (value) { case 'PREA1': + case 'PRE-A1': case 'Pre-A1': return LanguageLevelTypeEnum.preA1; case 'A1': diff --git a/lib/pangea/toolbar/enums/activity_type_enum.dart b/lib/pangea/toolbar/enums/activity_type_enum.dart index 86336c639..1d91cc51d 100644 --- a/lib/pangea/toolbar/enums/activity_type_enum.dart +++ b/lib/pangea/toolbar/enums/activity_type_enum.dart @@ -13,7 +13,7 @@ enum ActivityTypeEnum { lemmaId, emoji, morphId, - // correctionPuzzle, + messageMeaning, // TODO: Add to L10n } extension ActivityTypeExtension on ActivityTypeEnum { @@ -31,6 +31,8 @@ extension ActivityTypeExtension on ActivityTypeEnum { return 'emoji'; case ActivityTypeEnum.morphId: return 'morph_id'; + case ActivityTypeEnum.messageMeaning: + return 'message_meaning'; // TODO: Add to L10n } } @@ -41,6 +43,7 @@ extension ActivityTypeExtension on ActivityTypeEnum { case ActivityTypeEnum.lemmaId: case ActivityTypeEnum.emoji: case ActivityTypeEnum.morphId: + case ActivityTypeEnum.messageMeaning: return false; case ActivityTypeEnum.hiddenWordListening: return true; @@ -53,6 +56,7 @@ extension ActivityTypeExtension on ActivityTypeEnum { case ActivityTypeEnum.lemmaId: case ActivityTypeEnum.emoji: case ActivityTypeEnum.morphId: + case ActivityTypeEnum.messageMeaning: return false; case ActivityTypeEnum.wordFocusListening: case ActivityTypeEnum.hiddenWordListening: @@ -83,6 +87,8 @@ extension ActivityTypeExtension on ActivityTypeEnum { return ActivityTypeEnum.emoji; case 'morph_id': return ActivityTypeEnum.morphId; + case 'message_meaning': + return ActivityTypeEnum.messageMeaning; // TODO: Add to L10n default: throw Exception('Unknown activity type: $split'); } @@ -122,6 +128,12 @@ extension ActivityTypeExtension on ActivityTypeEnum { ConstructUseTypeEnum.incM, ConstructUseTypeEnum.ignM, ]; + case ActivityTypeEnum.messageMeaning: + return [ + ConstructUseTypeEnum.corMM, + ConstructUseTypeEnum.incMM, + ConstructUseTypeEnum.ignMM, + ]; // TODO: Add to L10n } } @@ -139,6 +151,8 @@ extension ActivityTypeExtension on ActivityTypeEnum { return ConstructUseTypeEnum.em; case ActivityTypeEnum.morphId: return ConstructUseTypeEnum.corM; + case ActivityTypeEnum.messageMeaning: + return ConstructUseTypeEnum.corMM; } } @@ -150,6 +164,7 @@ extension ActivityTypeExtension on ActivityTypeEnum { case ActivityTypeEnum.hiddenWordListening: case ActivityTypeEnum.lemmaId: case ActivityTypeEnum.emoji: + case ActivityTypeEnum.messageMeaning: return (id) => id.type == ConstructTypeEnum.vocab; case ActivityTypeEnum.morphId: return (id) => id.type == ConstructTypeEnum.morph; @@ -169,6 +184,8 @@ extension ActivityTypeExtension on ActivityTypeEnum { return Icons.emoji_emotions; case ActivityTypeEnum.morphId: return Icons.format_shapes; + case ActivityTypeEnum.messageMeaning: + return Icons.star; // TODO: Add to L10n } } } diff --git a/lib/pangea/toolbar/enums/message_mode_enum.dart b/lib/pangea/toolbar/enums/message_mode_enum.dart index b929baa59..4d2a3cc57 100644 --- a/lib/pangea/toolbar/enums/message_mode_enum.dart +++ b/lib/pangea/toolbar/enums/message_mode_enum.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:matrix/matrix.dart'; enum MessageMode { practiceActivity, @@ -11,6 +10,7 @@ enum MessageMode { speechToText, wordZoom, noneSelected, + messageMeaning, } extension MessageModeExtension on MessageMode { @@ -28,6 +28,8 @@ extension MessageModeExtension on MessageMode { return Symbols.dictionary; case MessageMode.noneSelected: return Icons.error; + case MessageMode.messageMeaning: + return Icons.star; } } @@ -45,6 +47,8 @@ extension MessageModeExtension on MessageMode { return L10n.of(context).vocab; case MessageMode.noneSelected: return ''; + case MessageMode.messageMeaning: + return L10n.of(context).meaning; } } @@ -62,21 +66,8 @@ extension MessageModeExtension on MessageMode { return L10n.of(context).vocab; case MessageMode.noneSelected: return ''; - } - } - - bool shouldShowAsToolbarButton(Event event) { - switch (this) { - case MessageMode.translation: - case MessageMode.textToSpeech: - return event.messageType == MessageTypes.Text; - case MessageMode.speechToText: - return event.messageType == MessageTypes.Audio; - case MessageMode.practiceActivity: - return true; - case MessageMode.wordZoom: - case MessageMode.noneSelected: - return false; + case MessageMode.messageMeaning: + return L10n.of(context).meaning; } } @@ -91,6 +82,7 @@ extension MessageModeExtension on MessageMode { case MessageMode.speechToText: case MessageMode.wordZoom: case MessageMode.noneSelected: + case MessageMode.messageMeaning: return 0; } } @@ -109,6 +101,7 @@ extension MessageModeExtension on MessageMode { case MessageMode.practiceActivity: case MessageMode.wordZoom: case MessageMode.noneSelected: + case MessageMode.messageMeaning: return true; } } diff --git a/lib/pangea/toolbar/models/message_activity_request.dart b/lib/pangea/toolbar/models/message_activity_request.dart index c7882c7fe..c48b3641f 100644 --- a/lib/pangea/toolbar/models/message_activity_request.dart +++ b/lib/pangea/toolbar/models/message_activity_request.dart @@ -1,3 +1,7 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; + import 'package:collection/collection.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -75,6 +79,7 @@ class MessageActivityRequest { if ([ActivityTypeEnum.wordFocusListening, ActivityTypeEnum.wordMeaning] .contains(targetType) && targetTokens.length > 1) { + debugger(when: kDebugMode); throw Exception( 'Target tokens must be a single token for this activity type', ); diff --git a/lib/pangea/toolbar/models/practice_activity_model.dart b/lib/pangea/toolbar/models/practice_activity_model.dart index 2b034091d..35862abe4 100644 --- a/lib/pangea/toolbar/models/practice_activity_model.dart +++ b/lib/pangea/toolbar/models/practice_activity_model.dart @@ -174,6 +174,7 @@ class PracticeActivityModel { case ActivityTypeEnum.hiddenWordListening: case ActivityTypeEnum.wordFocusListening: case ActivityTypeEnum.lemmaId: + case ActivityTypeEnum.messageMeaning: return content.question; case ActivityTypeEnum.emoji: return L10n.of(context).pickAnEmoji(targetLemma, partOfSpeech); diff --git a/lib/pangea/toolbar/models/practice_activity_record_model.dart b/lib/pangea/toolbar/models/practice_activity_record_model.dart index 3fca7daf4..df3b4fb84 100644 --- a/lib/pangea/toolbar/models/practice_activity_record_model.dart +++ b/lib/pangea/toolbar/models/practice_activity_record_model.dart @@ -163,6 +163,10 @@ class ActivityRecordResponse { return score > 0 ? ConstructUseTypeEnum.corHWL : ConstructUseTypeEnum.incHWL; + case ActivityTypeEnum.messageMeaning: + return score > 0 + ? ConstructUseTypeEnum.corMM + : ConstructUseTypeEnum.incMM; } } @@ -199,6 +203,15 @@ class ActivityRecordResponse { category: token.pos, ), ]; + case ActivityTypeEnum.messageMeaning: + return practiceActivity.targetTokens! + .expand( + (t) => t.allUses( + useType(practiceActivity.activityType), + metadata, + ), + ) + .toList(); case ActivityTypeEnum.hiddenWordListening: return practiceActivity.targetTokens! .map( diff --git a/lib/pangea/toolbar/repo/practice_repo.dart b/lib/pangea/toolbar/repo/practice_repo.dart index 9820f8892..0a9450847 100644 --- a/lib/pangea/toolbar/repo/practice_repo.dart +++ b/lib/pangea/toolbar/repo/practice_repo.dart @@ -133,7 +133,9 @@ class PracticeGenerationController { case ActivityTypeEnum.morphId: return _morph.get(req); case ActivityTypeEnum.wordMeaning: + debugger(when: kDebugMode); return _wordMeaning.get(req); + case ActivityTypeEnum.messageMeaning: case ActivityTypeEnum.wordFocusListening: case ActivityTypeEnum.hiddenWordListening: return _fetchFromServer( diff --git a/lib/pangea/toolbar/repo/word_meaning_static_practice_activity_model.dart b/lib/pangea/toolbar/repo/word_meaning_static_practice_activity_model.dart new file mode 100644 index 000000000..ada7a2371 --- /dev/null +++ b/lib/pangea/toolbar/repo/word_meaning_static_practice_activity_model.dart @@ -0,0 +1,59 @@ +import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/lemmas/lemma.dart'; +import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart'; +import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart'; +import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart'; + +final wordMeaningStaticPracticeActivityModel = MessageActivityResponse( + activity: PracticeActivityModel( + tgtConstructs: [ + ConstructIdentifier( + type: ConstructTypeEnum.vocab, + lemma: 'example', + category: 'noun', + ), + ], + targetTokens: [ + PangeaToken( + text: PangeaTokenText(content: 'Cómo', offset: 0, length: 4), + lemma: Lemma(text: 'cómo', saveVocab: true, form: 'cómo'), + pos: 'ADV', + morph: { + 'PronType': 'Int', + }, + ), + PangeaToken( + text: PangeaTokenText(content: 'estás', offset: 5, length: 5), + lemma: Lemma(text: 'estar', saveVocab: true, form: 'estás'), + pos: 'VERB', + morph: { + 'Mood': 'Ind', + 'Tense': 'Pres', + 'VerbForm': 'Fin', + 'Number': 'Sing', + 'Person': '2', + }, + ), + PangeaToken( + text: PangeaTokenText(content: '?', offset: 10, length: 1), + lemma: Lemma(text: '?', saveVocab: false, form: '?'), + pos: 'PUNCT', + morph: { + 'PunctType': 'Peri', + }, + ), + ], + langCode: 'en', + activityType: ActivityTypeEnum.messageMeaning, + content: ActivityContent( + question: 'What is the meaning of the message?', + choices: ['How are you?', 'What is your name?'], + answers: ['How are you?'], + spanDisplayDetails: null, + ), + ), +); diff --git a/lib/pangea/toolbar/widgets/message_meaning_button.dart b/lib/pangea/toolbar/widgets/message_meaning_button.dart new file mode 100644 index 000000000..044d913a8 --- /dev/null +++ b/lib/pangea/toolbar/widgets/message_meaning_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button.dart'; + +class MessageMeaningButton extends StatelessWidget { + final MessageOverlayController overlayController; + final double buttonSize; + + const MessageMeaningButton({ + super.key, + required this.overlayController, + required this.buttonSize, + }); + + @override + Widget build(BuildContext context) { + return AnimatedCrossFade( + crossFadeState: overlayController.isPracticeComplete + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: FluffyThemes.animationDuration, + firstChild: ToolbarButton( + mode: MessageMode.messageMeaning, + overlayController: overlayController, + buttonSize: buttonSize, + ), + secondChild: Container( + width: buttonSize, + height: buttonSize, + alignment: Alignment.center, + child: Icon( + MessageMode.messageMeaning.icon, + color: AppConfig.gold, + size: buttonSize, + ), + ), + ); + } +} diff --git a/lib/pangea/toolbar/widgets/message_meaning_card.dart b/lib/pangea/toolbar/widgets/message_meaning_card.dart new file mode 100644 index 000000000..931746738 --- /dev/null +++ b/lib/pangea/toolbar/widgets/message_meaning_card.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; + +class MessageMeaningCard extends StatelessWidget { + final MessageOverlayController controller; + + const MessageMeaningCard({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: AppConfig.toolbarMinWidth, + maxHeight: AppConfig.toolbarMaxHeight, + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.sports_martial_arts, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + TextButton( + onPressed: () => controller.onRequestForMeaningChallenge(), + child: Text(L10n.of(context).clickForMeaningActivity), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/toolbar/widgets/message_mode_locked_card.dart b/lib/pangea/toolbar/widgets/message_mode_locked_card.dart index 64272cfff..84cad27c6 100644 --- a/lib/pangea/toolbar/widgets/message_mode_locked_card.dart +++ b/lib/pangea/toolbar/widgets/message_mode_locked_card.dart @@ -3,9 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; class MessageModeLockedCard extends StatelessWidget { - const MessageModeLockedCard({super.key}); + final MessageOverlayController controller; + + const MessageModeLockedCard({super.key, required this.controller}); @override Widget build(BuildContext context) { diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index f25d093fa..79e85ed2f 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -173,6 +175,22 @@ class MessageOverlayController extends State } } + void onRequestForMeaningChallenge() { + if (messageAnalyticsEntry == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: "MessageAnalyticsEntry is null in onRequestForMeaningChallenge", + data: {}, + ); + return; + } + messageAnalyticsEntry!.addMessageMeaningActivity(); + + if (mounted) { + setState(() {}); + } + } + bool get messageInUserL2 => pangeaMessageEvent?.messageDisplayLangCode == MatrixState.pangeaController.languageController.userL2?.langCode; @@ -240,8 +258,12 @@ class MessageOverlayController extends State /// When an activity is completed, we need to update the state /// and check if the toolbar should be unlocked - void onActivityFinish() { + void onActivityFinish(ActivityTypeEnum activityType) { messageAnalyticsEntry!.onActivityComplete(); + if (activityType == ActivityTypeEnum.messageMeaning) { + updateToolbarMode(MessageMode.wordZoom); + } + if (!mounted) return; setState(() {}); } diff --git a/lib/pangea/toolbar/widgets/message_token_text.dart b/lib/pangea/toolbar/widgets/message_token_text.dart index d3e72dbca..81713f109 100644 --- a/lib/pangea/toolbar/widgets/message_token_text.dart +++ b/lib/pangea/toolbar/widgets/message_token_text.dart @@ -172,7 +172,9 @@ class MessageTextWidget extends StatelessWidget { ); } - final hasHiddenTokens = tokenPositions.any((t) => t.hideContent); + final hideTokenHighlights = messageAnalyticsEntry != null && + (messageAnalyticsEntry!.hasHiddenWordActivity || + messageAnalyticsEntry!.hasMessageMeaningActivity); return RichText( softWrap: softWrap ?? true, @@ -200,7 +202,7 @@ class MessageTextWidget extends StatelessWidget { .toString(); Color backgroundColor = Colors.transparent; - if (!hasHiddenTokens) { + if (!hideTokenHighlights) { if (tokenPosition.selected) { backgroundColor = AppConfig.primaryColor.withAlpha(80); } else if (isSelected != null && shouldDo) { diff --git a/lib/pangea/toolbar/widgets/message_toolbar.dart b/lib/pangea/toolbar/widgets/message_toolbar.dart index c26df8d07..51646a22e 100644 --- a/lib/pangea/toolbar/widgets/message_toolbar.dart +++ b/lib/pangea/toolbar/widgets/message_toolbar.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_meaning_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_speech_to_text_card.dart'; @@ -44,8 +45,10 @@ class MessageToolbar extends StatelessWidget { ); } - if (overlayController.messageAnalyticsEntry?.hasHiddenWordActivity ?? - false) { + if ((overlayController.messageAnalyticsEntry?.hasHiddenWordActivity ?? + false) || + (overlayController.messageAnalyticsEntry?.hasMessageMeaningActivity ?? + false)) { return PracticeActivityCard( pangeaMessageEvent: pangeaMessageEvent, overlayController: overlayController, @@ -64,7 +67,7 @@ class MessageToolbar extends StatelessWidget { ); if (!unlocked) { - return const MessageModeLockedCard(); + return MessageModeLockedCard(controller: overlayController); } switch (overlayController.toolbarMode) { @@ -93,6 +96,8 @@ class MessageToolbar extends StatelessWidget { textAlign: TextAlign.center, ), ); + case MessageMode.messageMeaning: + return MessageMeaningCard(controller: overlayController); case MessageMode.practiceActivity: case MessageMode.wordZoom: if (overlayController.selectedToken == null) { diff --git a/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart index 63b20438d..3e8884974 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart @@ -256,7 +256,9 @@ class MultipleChoiceActivityState extends State { ], ); - return practiceActivity.activityType == ActivityTypeEnum.hiddenWordListening + return practiceActivity.activityType == + ActivityTypeEnum.hiddenWordListening || + practiceActivity.activityType == ActivityTypeEnum.messageMeaning ? ConstrainedBox( constraints: const BoxConstraints( // see https://github.com/pangeachat/client/issues/1422 diff --git a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart index d1220b341..ec12ccd42 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart @@ -103,6 +103,7 @@ class PracticeActivityCardState extends State { ActivityQualityFeedback? activityFeedback, }) async { _error = null; + debugger(when: kDebugMode); if (!mounted || !pangeaController.languageController.languagesSet || widget.overlayController.messageAnalyticsEntry == null) { @@ -152,6 +153,7 @@ class PracticeActivityCardState extends State { Future _fetchActivityModel({ ActivityQualityFeedback? activityFeedback, }) async { + debugger(when: kDebugMode); debugPrint( "fetching activity model of type: ${widget.targetTokensAndActivityType.activityType}", ); @@ -215,6 +217,8 @@ class PracticeActivityCardState extends State { context, ); + debugger(when: kDebugMode); + if (activityResponse.activity == null) return null; currentActivityCompleter = activityResponse.eventCompleter; @@ -274,7 +278,7 @@ class PracticeActivityCardState extends State { // wait for savor the joy before popping from the activity queue // to keep the completed activity on screen for a moment - widget.overlayController.onActivityFinish(); + widget.overlayController.onActivityFinish(currentActivity!.activityType); } catch (e, s) { _onError(); debugger(when: kDebugMode); @@ -309,6 +313,7 @@ class PracticeActivityCardState extends State { case ActivityTypeEnum.lemmaId: case ActivityTypeEnum.emoji: case ActivityTypeEnum.morphId: + case ActivityTypeEnum.messageMeaning: final selectedChoice = currentActivity?.activityType == ActivityTypeEnum.emoji && (currentActivity?.targetTokens?.isNotEmpty ?? false) @@ -330,6 +335,7 @@ class PracticeActivityCardState extends State { @override Widget build(BuildContext context) { if (_error != null) { + debugger(when: kDebugMode); return CardErrorWidget( error: _error!, maxWidth: 500, diff --git a/lib/pangea/toolbar/widgets/toolbar_button.dart b/lib/pangea/toolbar/widgets/toolbar_button.dart index 929674c9e..7096bee0f 100644 --- a/lib/pangea/toolbar/widgets/toolbar_button.dart +++ b/lib/pangea/toolbar/widgets/toolbar_button.dart @@ -33,32 +33,26 @@ class ToolbarButton extends StatelessWidget { Widget build(BuildContext context) { return Tooltip( message: mode.tooltip(context), - child: Stack( - alignment: Alignment.center, - children: [ - PressableButton( - borderRadius: BorderRadius.circular(20), - depressed: mode == overlayController.toolbarMode, + child: PressableButton( + borderRadius: BorderRadius.circular(20), + depressed: mode == overlayController.toolbarMode, + color: color(context), + onPressed: () => overlayController.updateToolbarMode(mode), + playSound: true, + child: AnimatedContainer( + duration: FluffyThemes.animationDuration, + height: buttonSize, + width: buttonSize, + decoration: BoxDecoration( color: color(context), - onPressed: () => overlayController.updateToolbarMode(mode), - playSound: true, - child: AnimatedContainer( - duration: FluffyThemes.animationDuration, - height: buttonSize, - width: buttonSize, - decoration: BoxDecoration( - color: color(context), - shape: BoxShape.circle, - ), - child: Icon( - mode.icon, - size: 20, - color: - mode == overlayController.toolbarMode ? Colors.white : null, - ), - ), + shape: BoxShape.circle, ), - ], + child: Icon( + mode.icon, + size: 20, + color: mode == overlayController.toolbarMode ? Colors.white : null, + ), + ), ), ); } diff --git a/lib/pangea/toolbar/widgets/toolbar_button_and_progress_row.dart b/lib/pangea/toolbar/widgets/toolbar_button_and_progress_row.dart index 455954fb1..c63c7d842 100644 --- a/lib/pangea/toolbar/widgets/toolbar_button_and_progress_row.dart +++ b/lib/pangea/toolbar/widgets/toolbar_button_and_progress_row.dart @@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_meaning_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button.dart'; @@ -23,10 +24,6 @@ class ToolbarButtonAndProgressRow extends StatelessWidget { double? get proportionOfActivitiesCompleted => overlayController.pangeaMessageEvent?.proportionOfActivitiesCompleted; - List get modes => MessageMode.values - .where((mode) => mode.shouldShowAsToolbarButton(event)) - .toList(); - static const double iconWidth = 36.0; static const double buttonSize = 40.0; static const double totalRowWidth = 250.0; @@ -77,11 +74,14 @@ class ToolbarButtonAndProgressRow extends StatelessWidget { ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + MessageMeaningButton( + buttonSize: buttonSize, + overlayController: overlayController, + ), SizedBox( width: MessageMode.textToSpeech.pointOnBar * totalRowWidth - - buttonSize / 2, + buttonSize, ), ToolbarButton( mode: MessageMode.textToSpeech, @@ -91,7 +91,7 @@ class ToolbarButtonAndProgressRow extends StatelessWidget { SizedBox( width: MessageMode.translation.pointOnBar * totalRowWidth - MessageMode.textToSpeech.pointOnBar * totalRowWidth - - buttonSize / 2 - + buttonSize - buttonSize, ), ToolbarButton( diff --git a/pubspec.lock b/pubspec.lock index f675b46b7..61dbd9e22 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0" + base58check: + dependency: transitive + description: + name: base58check + sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" blurhash_dart: dependency: "direct main" description: @@ -185,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.3" + canonical_json: + dependency: transitive + description: + name: canonical_json + sha256: d6be1dd66b420c6ac9f42e3693e09edf4ff6edfee26cb4c28c1c019fdb8c0c15 + url: "https://pub.dev" + source: hosted + version: "1.1.2" characters: dependency: "direct main" description: @@ -417,6 +433,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.9" + enhanced_enum: + dependency: transitive + description: + name: enhanced_enum + sha256: "074c5a8b9664799ca91e1e8b68003b8694cb19998671cbafd9c7779c13fcdecf" + url: "https://pub.dev" + source: hosted + version: "0.2.4" equatable: dependency: transitive description: @@ -1095,6 +1119,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.4" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: "direct main" description: @@ -1493,6 +1525,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + olm: + dependency: transitive + description: + name: olm + sha256: "3306bf534ceb914fd148b3b4a3d603fb5e067b2e6da8304025b47c24cfdf6b46" + url: "https://pub.dev" + source: hosted + version: "2.0.4" open_file: dependency: "direct main" description: @@ -1845,6 +1885,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + random_string: + dependency: transitive + description: + name: random_string + sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02" + url: "https://pub.dev" + source: hosted + version: "2.3.1" receive_sharing_intent: dependency: "direct main" description: @@ -1957,6 +2005,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + sdp_transform: + dependency: transitive + description: + name: sdp_transform + sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45" + url: "https://pub.dev" + source: hosted + version: "0.3.2" sentry: dependency: transitive description: @@ -2434,6 +2490,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" + url: "https://pub.dev" + source: hosted + version: "0.2.0" url_launcher: dependency: "direct main" description: