From 1dcd988be0e2920330b81b240f88afe2ef743de5 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Thu, 6 Jun 2024 18:05:16 -0400 Subject: [PATCH 01/15] skeleton of practice activities --- assets/l10n/intl_en.arb | 3 +- lib/pangea/constants/pangea_event_types.dart | 3 + lib/pangea/enum/message_mode_enum.dart | 15 +- .../extensions/pangea_event_extension.dart | 2 + .../pangea_message_event.dart | 50 ++++ .../practice_activity_event.dart | 29 +++ .../multiple_choice_activity_model.dart | 62 +++++ .../practice_activity_model.dart | 223 ++++++++++++++++++ lib/pangea/widgets/chat/message_toolbar.dart | 13 + .../message_practice_activity_card.dart | 38 +++ .../multiple_choice_activity.dart | 81 +++++++ needed-translations.txt | 145 ++++++++---- 12 files changed, 615 insertions(+), 49 deletions(-) create mode 100644 lib/pangea/matrix_event_wrappers/practice_activity_event.dart create mode 100644 lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart create mode 100644 lib/pangea/models/practice_activities.dart/practice_activity_model.dart create mode 100644 lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart create mode 100644 lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 1d5ff4218..0bb035da0 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3963,5 +3963,6 @@ "studentAnalyticsNotAvailable": "Student data not currently available", "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", "updatePhoneOS": "You may need to update your device's OS version.", - "wordsPerMinute": "Words per minute" + "wordsPerMinute": "Words per minute", + "practice": "Practice" } \ No newline at end of file diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart index cfdb7f0d7..df37724b3 100644 --- a/lib/pangea/constants/pangea_event_types.dart +++ b/lib/pangea/constants/pangea_event_types.dart @@ -23,4 +23,7 @@ class PangeaEventTypes { static const String report = 'm.report'; static const textToSpeechRule = "p.rule.text_to_speech"; + + static const activityResponse = "pangea.activity_res"; + static const acitivtyRequest = "pangea.activity_req"; } diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart index 25948d23b..f25140a9c 100644 --- a/lib/pangea/enum/message_mode_enum.dart +++ b/lib/pangea/enum/message_mode_enum.dart @@ -3,7 +3,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix.dart'; -enum MessageMode { translation, definition, speechToText, textToSpeech } +enum MessageMode { + translation, + definition, + speechToText, + textToSpeech, + practiceActivity +} extension MessageModeExtension on MessageMode { IconData get icon { @@ -17,6 +23,8 @@ extension MessageModeExtension on MessageMode { //TODO change icon for audio messages case MessageMode.definition: return Icons.book; + case MessageMode.practiceActivity: + return Symbols.fitness_center; default: return Icons.error; // Icon to indicate an error or unsupported mode } @@ -32,6 +40,8 @@ extension MessageModeExtension on MessageMode { return L10n.of(context)!.speechToTextTooltip; case MessageMode.definition: return L10n.of(context)!.definitions; + case MessageMode.practiceActivity: + return L10n.of(context)!.practice; default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode @@ -48,6 +58,8 @@ extension MessageModeExtension on MessageMode { return L10n.of(context)!.speechToTextTooltip; case MessageMode.definition: return L10n.of(context)!.define; + case MessageMode.practiceActivity: + return L10n.of(context)!.practice; default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode @@ -58,6 +70,7 @@ extension MessageModeExtension on MessageMode { switch (this) { case MessageMode.translation: case MessageMode.textToSpeech: + case MessageMode.practiceActivity: case MessageMode.definition: return event.messageType == MessageTypes.Text; case MessageMode.speechToText: diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart index f62f27925..dcc3a8bec 100644 --- a/lib/pangea/extensions/pangea_event_extension.dart +++ b/lib/pangea/extensions/pangea_event_extension.dart @@ -26,6 +26,8 @@ extension PangeaEvent on Event { return PangeaRepresentation.fromJson(json) as V; case PangeaEventTypes.choreoRecord: return ChoreoRecord.fromJson(json) as V; + case PangeaEventTypes.activityResponse: + return PangeaMessageTokens.fromJson(json) as V; default: throw Exception("$type events do not have pangea content"); } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 5fa2e2659..c874dc93c 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -4,11 +4,15 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; @@ -601,6 +605,52 @@ class PangeaMessageEvent { return steps; } + List get _practiceActivityEvents => _latestEdit + .aggregatedEvents( + timeline, + PangeaEventTypes.activityResponse, + ) + .map( + (e) => PracticeActivityEvent( + event: e, + ), + ) + .toList(); + + List activities(String langCode) { + // final List practiceActivityEvents = _practiceActivityEvents; + + // final List activities = _practiceActivityEvents + // .map( + // (e) => PracticeActivityModel.fromJson( + // e.event.content, + // ), + // ) + // .where( + // (element) => element.langCode == langCode, + // ) + // .toList(); + + // return activities; + + // for now, return a hard-coded activity + final PracticeActivityModel activityModel = PracticeActivityModel( + tgtConstructs: [ + ConstructIdentifier(lemma: "be", type: ConstructType.vocab.string), + ], + activityType: ActivityType.multipleChoice, + langCode: langCode, + msgId: _event.eventId, + multipleChoice: MultipleChoice( + question: "What is a synonym for 'happy'?", + choices: ["sad", "angry", "joyful", "tired"], + correctAnswer: "joyful", + ), + ); + + return [activityModel]; + } + // List get activities => //each match is turned into an activity that other students can access //they're not told the answer but have to find it themselves diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart new file mode 100644 index 000000000..3e5fa5f16 --- /dev/null +++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart @@ -0,0 +1,29 @@ +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; + +class PracticeActivityEvent { + Event event; + PracticeActivityModel? _content; + + PracticeActivityEvent({required this.event}) { + if (event.type != PangeaEventTypes.activityResponse) { + throw Exception( + "${event.type} should not be used to make a PracticeActivityEvent", + ); + } + } + + PracticeActivityModel? get practiceActivity { + try { + _content ??= event.getPangeaContent(); + return _content!; + } catch (err, s) { + ErrorHandler.logError(e: err, s: s); + return null; + } + } +} diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart new file mode 100644 index 000000000..a0007e754 --- /dev/null +++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart @@ -0,0 +1,62 @@ +class MultipleChoice { + final String question; + final List choices; + final String correctAnswer; + + MultipleChoice({ + required this.question, + required this.choices, + required this.correctAnswer, + }); + + bool get isValidQuestion => choices.contains(correctAnswer); + + int get correctAnswerIndex => choices.indexOf(correctAnswer); + + factory MultipleChoice.fromJson(Map json) { + return MultipleChoice( + question: json['question'] as String, + choices: (json['choices'] as List).map((e) => e as String).toList(), + correctAnswer: json['correct_answer'] as String, + ); + } + + Map toJson() { + return { + 'question': question, + 'choices': choices, + 'correct_answer': correctAnswer, + }; + } +} + +// record the options that the user selected +// note that this is not the same as the correct answer +// the user might have selected multiple options before +// finding the answer +class MultipleChoiceActivityCompletionRecord { + final String question; + List selectedOptions; + + MultipleChoiceActivityCompletionRecord({ + required this.question, + this.selectedOptions = const [], + }); + + factory MultipleChoiceActivityCompletionRecord.fromJson( + Map json, + ) { + return MultipleChoiceActivityCompletionRecord( + question: json['question'] as String, + selectedOptions: + (json['selected_options'] as List).map((e) => e as String).toList(), + ); + } + + Map toJson() { + return { + 'question': question, + 'selected_options': selectedOptions, + }; + } +} diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart new file mode 100644 index 000000000..10aaefa87 --- /dev/null +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -0,0 +1,223 @@ +import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; + +class ConstructIdentifier { + final String lemma; + final String type; + + ConstructIdentifier({required this.lemma, required this.type}); + + factory ConstructIdentifier.fromJson(Map json) { + return ConstructIdentifier( + lemma: json['lemma'] as String, + type: json['type'] as String, + ); + } + + Map toJson() { + return { + 'lemma': lemma, + 'type': type, + }; + } +} + +enum ActivityType { multipleChoice, freeResponse, listening, speaking } + +class MessageInfo { + final String msgId; + final String roomId; + final String text; + + MessageInfo({required this.msgId, required this.roomId, required this.text}); + + factory MessageInfo.fromJson(Map json) { + return MessageInfo( + msgId: json['msg_id'] as String, + roomId: json['room_id'] as String, + text: json['text'] as String, + ); + } + + Map toJson() { + return { + 'msg_id': msgId, + 'room_id': roomId, + 'text': text, + }; + } +} + +class ActivityRequest { + final String mode; + final List? targetConstructs; + final List? candidateMessages; + final List? userIds; + final ActivityType? activityType; + final int numActivities; + + ActivityRequest({ + required this.mode, + this.targetConstructs, + this.candidateMessages, + this.userIds, + this.activityType, + this.numActivities = 10, + }); + + factory ActivityRequest.fromJson(Map json) { + return ActivityRequest( + mode: json['mode'] as String, + targetConstructs: (json['target_constructs'] as List?) + ?.map((e) => ConstructIdentifier.fromJson(e as Map)) + .toList(), + candidateMessages: (json['candidate_msgs'] as List) + .map((e) => MessageInfo.fromJson(e as Map)) + .toList(), + userIds: (json['user_ids'] as List?)?.map((e) => e as String).toList(), + activityType: ActivityType.values.firstWhere( + (e) => e.toString().split('.').last == json['activity_type'], + ), + numActivities: json['num_activities'] as int, + ); + } + + Map toJson() { + return { + 'mode': mode, + 'target_constructs': targetConstructs?.map((e) => e.toJson()).toList(), + 'candidate_msgs': candidateMessages?.map((e) => e.toJson()).toList(), + 'user_ids': userIds, + 'activity_type': activityType?.toString().split('.').last, + 'num_activities': numActivities, + }; + } +} + +class FreeResponse { + final String question; + final String correctAnswer; + final String gradingGuide; + + FreeResponse({ + required this.question, + required this.correctAnswer, + required this.gradingGuide, + }); + + factory FreeResponse.fromJson(Map json) { + return FreeResponse( + question: json['question'] as String, + correctAnswer: json['correct_answer'] as String, + gradingGuide: json['grading_guide'] as String, + ); + } + + Map toJson() { + return { + 'question': question, + 'correct_answer': correctAnswer, + 'grading_guide': gradingGuide, + }; + } +} + +class Listening { + final String audioUrl; + final String text; + + Listening({required this.audioUrl, required this.text}); + + factory Listening.fromJson(Map json) { + return Listening( + audioUrl: json['audio_url'] as String, + text: json['text'] as String, + ); + } + + Map toJson() { + return { + 'audio_url': audioUrl, + 'text': text, + }; + } +} + +class Speaking { + final String text; + + Speaking({required this.text}); + + factory Speaking.fromJson(Map json) { + return Speaking( + text: json['text'] as String, + ); + } + + Map toJson() { + return { + 'text': text, + }; + } +} + +class PracticeActivityModel { + final List tgtConstructs; + final String langCode; + final String msgId; + final ActivityType activityType; + final MultipleChoice? multipleChoice; + final Listening? listening; + final Speaking? speaking; + final FreeResponse? freeResponse; + + PracticeActivityModel({ + required this.tgtConstructs, + required this.langCode, + required this.msgId, + required this.activityType, + this.multipleChoice, + this.listening, + this.speaking, + this.freeResponse, + }); + + factory PracticeActivityModel.fromJson(Map json) { + return PracticeActivityModel( + tgtConstructs: (json['tgt_constructs'] as List) + .map((e) => ConstructIdentifier.fromJson(e as Map)) + .toList(), + langCode: json['lang_code'] as String, + msgId: json['msg_id'] as String, + activityType: ActivityType.values.firstWhere( + (e) => e.toString().split('.').last == json['activity_type'], + ), + multipleChoice: json['multiple_choice'] != null + ? MultipleChoice.fromJson( + json['multiple_choice'] as Map, + ) + : null, + listening: json['listening'] != null + ? Listening.fromJson(json['listening'] as Map) + : null, + speaking: json['speaking'] != null + ? Speaking.fromJson(json['speaking'] as Map) + : null, + freeResponse: json['free_response'] != null + ? FreeResponse.fromJson(json['free_response'] as Map) + : null, + ); + } + + Map toJson() { + return { + 'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(), + 'lang_code': langCode, + 'msg_id': msgId, + 'activity_type': activityType.toString().split('.').last, + 'multiple_choice': multipleChoice?.toJson(), + 'listening': listening?.toJson(), + 'speaking': speaking?.toJson(), + 'free_response': freeResponse?.toJson(), + }; + } +} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 142a27227..523637b37 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -215,6 +216,9 @@ class MessageToolbarState extends State { case MessageMode.definition: showDefinition(); break; + case MessageMode.practiceActivity: + showPracticeActivity(); + break; default: ErrorHandler.logError( e: "Invalid toolbar mode", @@ -272,6 +276,15 @@ class MessageToolbarState extends State { ); } + void showPracticeActivity() { + toolbarContent = PracticeActivityCard( + practiceActivity: widget.pangeaMessageEvent + // @ggurdin - is this the best way to get the l2 language here? + .activities(widget.pangeaMessageEvent.messageDisplayLangCode) + .first, + ); + } + void showImage() {} void spellCheck() {} diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart b/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart new file mode 100644 index 000000000..c5bb5dbfa --- /dev/null +++ b/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart @@ -0,0 +1,38 @@ +//stateful widget that displays a card with a practice activity + +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart'; +import 'package:flutter/material.dart'; + +class PracticeActivityCard extends StatefulWidget { + final PracticeActivityModel practiceActivity; + + const PracticeActivityCard({ + super.key, + required this.practiceActivity, + }); + + @override + MessagePracticeActivityCardState createState() => + MessagePracticeActivityCardState(); +} + +//parameters for the stateful widget +// practiceActivity: the practice activity to display +// use a switch statement based on the type of the practice activity to display the appropriate content +// just use different widgets for the different types, don't define in this file +// for multiple choice, use the MultipleChoiceActivity widget +// for the rest, just return SizedBox.shrink() for now +class MessagePracticeActivityCardState extends State { + @override + Widget build(BuildContext context) { + switch (widget.practiceActivity.activityType) { + case ActivityType.multipleChoice: + return MultipleChoiceActivity( + practiceActivity: widget.practiceActivity, + ); + default: + return const SizedBox.shrink(); + } + } +} diff --git a/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart new file mode 100644 index 000000000..f8bec5436 --- /dev/null +++ b/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart @@ -0,0 +1,81 @@ +// stateful widget that displays a card with a practice activity of type multiple choice + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:flutter/material.dart'; + +class MultipleChoiceActivity extends StatefulWidget { + final PracticeActivityModel practiceActivity; + + const MultipleChoiceActivity({ + super.key, + required this.practiceActivity, + }); + + @override + MultipleChoiceActivityState createState() => MultipleChoiceActivityState(); +} + +//parameters for the stateful widget +// practiceActivity: the practice activity to display +// show the question text and choices +// use the ChoiceArray widget to display the choices +class MultipleChoiceActivityState extends State { + int? selectedChoiceIndex; + + late MultipleChoiceActivityCompletionRecord? completionRecord; + + @override + initState() { + super.initState(); + selectedChoiceIndex = null; + completionRecord = MultipleChoiceActivityCompletionRecord( + question: widget.practiceActivity.multipleChoice!.question, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Text( + widget.practiceActivity.multipleChoice!.question, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + ChoicesArray( + isLoading: false, + uniqueKeyForLayerLink: (index) => "multiple_choice_$index", + onLongPress: null, + onPressed: (index) { + selectedChoiceIndex = index; + completionRecord!.selectedOptions + .add(widget.practiceActivity.multipleChoice!.choices[index]); + setState(() {}); + }, + originalSpan: "placeholder", + selectedChoiceIndex: selectedChoiceIndex, + choices: widget.practiceActivity.multipleChoice!.choices + .mapIndexed( + (int index, String value) => Choice( + text: value, + color: null, + isGold: + widget.practiceActivity.multipleChoice!.correctAnswer == + value, + ), + ) + .toList(), + ), + ], + ), + ); + } +} diff --git a/needed-translations.txt b/needed-translations.txt index bb967d011..6a6224756 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -839,7 +839,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "be": [ @@ -2277,7 +2278,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "bn": [ @@ -3177,7 +3179,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "bo": [ @@ -4077,7 +4080,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ca": [ @@ -4977,7 +4981,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "cs": [ @@ -5877,7 +5882,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "de": [ @@ -6724,7 +6730,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "el": [ @@ -7624,7 +7631,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "eo": [ @@ -8524,7 +8532,12 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" + ], + + "es": [ + "practice" ], "et": [ @@ -9367,7 +9380,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "eu": [ @@ -10210,7 +10224,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "fa": [ @@ -11110,7 +11125,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "fi": [ @@ -12010,7 +12026,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "fr": [ @@ -12910,7 +12927,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ga": [ @@ -13810,7 +13828,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "gl": [ @@ -14653,7 +14672,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "he": [ @@ -15553,7 +15573,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "hi": [ @@ -16453,7 +16474,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "hr": [ @@ -17340,7 +17362,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "hu": [ @@ -18240,7 +18263,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ia": [ @@ -19664,7 +19688,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "id": [ @@ -20564,7 +20589,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ie": [ @@ -21464,7 +21490,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "it": [ @@ -22349,7 +22376,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ja": [ @@ -23249,7 +23277,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ko": [ @@ -24149,7 +24178,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "lt": [ @@ -25049,7 +25079,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "lv": [ @@ -25949,7 +25980,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "nb": [ @@ -26849,7 +26881,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "nl": [ @@ -27749,7 +27782,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "pl": [ @@ -28649,7 +28683,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "pt": [ @@ -29549,7 +29584,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "pt_BR": [ @@ -30418,7 +30454,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "pt_PT": [ @@ -31318,7 +31355,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ro": [ @@ -32218,7 +32256,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ru": [ @@ -33061,7 +33100,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "sk": [ @@ -33961,7 +34001,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "sl": [ @@ -34861,7 +34902,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "sr": [ @@ -35761,7 +35803,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "sv": [ @@ -36626,7 +36669,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "ta": [ @@ -37526,7 +37570,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "th": [ @@ -38426,7 +38471,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "tr": [ @@ -39311,7 +39357,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "uk": [ @@ -40154,7 +40201,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "vi": [ @@ -41054,7 +41102,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "zh": [ @@ -41897,7 +41946,8 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ], "zh_Hant": [ @@ -42797,6 +42847,7 @@ "studentAnalyticsNotAvailable", "roomDataMissing", "updatePhoneOS", - "wordsPerMinute" + "wordsPerMinute", + "practice" ] } From 91d7600c5de9422fa64d8bb51a9f9a312ed549b4 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 12 Jun 2024 17:34:42 -0400 Subject: [PATCH 02/15] display, interactivity, saving/fetching of record, and dummy generation all done --- README.md | 2 +- assets/l10n/intl_en.arb | 3 +- lib/pages/chat/events/message.dart | 40 +++-- .../choreographer/widgets/choice_array.dart | 93 +++++------ lib/pangea/constants/pangea_event_types.dart | 3 +- lib/pangea/controllers/pangea_controller.dart | 7 + ...actice_activity_generation_controller.dart | 101 ++++++++++++ .../practice_activity_record_controller.dart | 94 ++++++++++++ .../extensions/pangea_event_extension.dart | 9 +- .../pangea_audio_events.dart | 9 -- .../pangea_choreo_event.dart | 4 +- .../pangea_message_event.dart | 81 +++++----- .../pangea_tokens_event.dart | 4 + .../practice_acitivity_record_event.dart | 24 +++ .../practice_activity_event.dart | 58 +++++-- .../multiple_choice_activity_model.dart | 39 +---- .../practice_activity_model.dart | 72 +++++++-- .../practice_activity_record_model.dart | 127 +++++++++++++++ lib/pangea/widgets/chat/message_buttons.dart | 96 ++++++++++++ lib/pangea/widgets/chat/message_toolbar.dart | 8 +- .../generate_practice_activity.dart | 60 ++++++++ .../message_practice_activity_card.dart | 67 +++++--- .../message_practice_activity_content.dart | 141 +++++++++++++++++ .../multiple_choice_activity.dart | 66 ++++---- .../user_settings/p_language_dialog.dart | 1 - needed-translations.txt | 144 ++++++++++++------ .../fcm_shared_isolate/pubspec.lock | 56 +++++-- 27 files changed, 1116 insertions(+), 293 deletions(-) create mode 100644 lib/pangea/controllers/practice_activity_generation_controller.dart create mode 100644 lib/pangea/controllers/practice_activity_record_controller.dart delete mode 100644 lib/pangea/matrix_event_wrappers/pangea_audio_events.dart create mode 100644 lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart create mode 100644 lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart create mode 100644 lib/pangea/widgets/chat/message_buttons.dart create mode 100644 lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart create mode 100644 lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart diff --git a/README.md b/README.md index 7c27b6e2e..a1ad9f2b7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ # Special thanks -* Pangea Chat is a fork of [FluffyChat](https://fluffychat.im), is an open source, nonprofit and cute [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). The goal of FluffyChat is to create an easy to use instant messenger which is open source and accessible for everyone. You can [support the primary maker of FluffyChat directly here.](https://ko-fi.com/C1C86VN53) +* Pangea Chat is a fork of [FluffyChat](https://fluffychat.im) which is a [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). You can [support the primary maker of FluffyChat directly here.](https://ko-fi.com/C1C86VN53) * Fabiyamada is a graphics designer and has made the fluffychat logo and the banner. Big thanks for her great designs. diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 0bb035da0..bf9a112b6 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3964,5 +3964,6 @@ "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", "updatePhoneOS": "You may need to update your device's OS version.", "wordsPerMinute": "Words per minute", - "practice": "Practice" + "practice": "Practice", + "noLanguagesSet": "Please set your target language and try again." } \ No newline at end of file diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 472ef4eb4..c604d9c19 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; @@ -108,7 +109,7 @@ class Message extends StatelessWidget { final client = Matrix.of(context).client; final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; - var color = Theme.of(context).colorScheme.surfaceVariant; + var color = Theme.of(context).colorScheme.surfaceContainerHighest; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); @@ -132,7 +133,7 @@ class Message extends StatelessWidget { final textColor = ownMessage ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onBackground; + : Theme.of(context).colorScheme.onSurface; final rowMainAxisAlignment = ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start; @@ -458,7 +459,14 @@ class Message extends StatelessWidget { Widget container; final showReceiptsRow = event.hasAggregatedEvents(timeline, RelationshipTypes.reaction); - if (showReceiptsRow || displayTime || selected || displayReadMarker) { + // #Pangea + // if (showReceiptsRow || displayTime || selected || displayReadMarker) { + if (showReceiptsRow || + displayTime || + selected || + displayReadMarker || + (pangeaMessageEvent?.showMessageButtons ?? false)) { + // Pangea# container = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: @@ -472,11 +480,8 @@ class Message extends StatelessWidget { child: Center( child: Material( color: displayTime - ? Theme.of(context).colorScheme.background - : Theme.of(context) - .colorScheme - .background - .withOpacity(0.33), + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.surface.withOpacity(0.33), borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), clipBehavior: Clip.antiAlias, @@ -498,7 +503,11 @@ class Message extends StatelessWidget { AnimatedSize( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - child: !showReceiptsRow + // #Pangea + child: !showReceiptsRow && + !(pangeaMessageEvent?.showMessageButtons ?? false) + // child: !showReceiptsRow + // Pangea# ? const SizedBox.shrink() : Padding( padding: EdgeInsets.only( @@ -506,7 +515,18 @@ class Message extends StatelessWidget { left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0, right: ownMessage ? 0 : 12.0, ), - child: MessageReactions(event, timeline), + // #Pangea + child: Row( + mainAxisAlignment: ownMessage + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + MessageButtons(toolbarController: toolbarController), + MessageReactions(event, timeline), + ], + ), + // child: MessageReactions(event, timeline), + // Pangea# ), ), if (displayReadMarker) diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index 54fd601b9..c26fd706d 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -3,9 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; import '../../utils/bot_style.dart'; import 'it_shimmer.dart'; @@ -18,6 +16,10 @@ class ChoicesArray extends StatelessWidget { final int? selectedChoiceIndex; final String originalSpan; final String Function(int) uniqueKeyForLayerLink; + + /// some uses of this widget want to disable the choices + final bool isActive; + const ChoicesArray({ super.key, required this.isLoading, @@ -26,6 +28,7 @@ class ChoicesArray extends StatelessWidget { required this.originalSpan, required this.uniqueKeyForLayerLink, required this.selectedChoiceIndex, + this.isActive = true, this.onLongPress, }); @@ -42,8 +45,8 @@ class ChoicesArray extends StatelessWidget { .map( (entry) => ChoiceItem( theme: theme, - onLongPress: onLongPress, - onPressed: onPressed, + onLongPress: isActive ? onLongPress : null, + onPressed: isActive ? onPressed : (_) {}, entry: entry, isSelected: selectedChoiceIndex == entry.key, ), @@ -109,19 +112,19 @@ class ChoiceItem extends StatelessWidget { : null, child: TextButton( style: ButtonStyle( - padding: MaterialStateProperty.all( + padding: WidgetStateProperty.all( const EdgeInsets.symmetric(horizontal: 7), ), //if index is selected, then give the background a slight primary color - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( entry.value.color != null ? entry.value.color!.withOpacity(0.2) : theme.colorScheme.primary.withOpacity(0.1), ), - textStyle: MaterialStateProperty.all( + textStyle: WidgetStateProperty.all( BotStyle.text(context), ), - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -177,21 +180,21 @@ class ChoiceAnimationWidgetState extends State ); _animation = widget.isGold - ? Tween(begin: 1.0, end: 1.2).animate(_controller) - : TweenSequence([ - TweenSequenceItem( - tween: Tween(begin: 0, end: -8 * pi / 180), - weight: 1.0, - ), - TweenSequenceItem( - tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), - weight: 2.0, - ), - TweenSequenceItem( - tween: Tween(begin: 16 * pi / 180, end: 0), - weight: 1.0, - ), - ]).animate(_controller); + ? Tween(begin: 1.0, end: 1.2).animate(_controller) + : TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0, end: -8 * pi / 180), + weight: 1.0, + ), + TweenSequenceItem( + tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), + weight: 2.0, + ), + TweenSequenceItem( + tween: Tween(begin: 16 * pi / 180, end: 0), + weight: 1.0, + ), + ]).animate(_controller); if (widget.selected && !animationPlayed) { _controller.forward(); @@ -221,28 +224,28 @@ class ChoiceAnimationWidgetState extends State @override Widget build(BuildContext context) { return widget.isGold - ? AnimatedBuilder( - key: UniqueKey(), - animation: _animation, - builder: (context, child) { - return Transform.scale( - scale: _animation.value, - child: child, - ); - }, - child: widget.child, - ) - : AnimatedBuilder( - key: UniqueKey(), - animation: _animation, - builder: (context, child) { - return Transform.rotate( - angle: _animation.value, - child: child, - ); - }, - child: widget.child, - ); + ? AnimatedBuilder( + key: UniqueKey(), + animation: _animation, + builder: (context, child) { + return Transform.scale( + scale: _animation.value, + child: child, + ); + }, + child: widget.child, + ) + : AnimatedBuilder( + key: UniqueKey(), + animation: _animation, + builder: (context, child) { + return Transform.rotate( + angle: _animation.value, + child: child, + ); + }, + child: widget.child, + ); } @override diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart index df37724b3..344bd3122 100644 --- a/lib/pangea/constants/pangea_event_types.dart +++ b/lib/pangea/constants/pangea_event_types.dart @@ -24,6 +24,7 @@ class PangeaEventTypes { static const String report = 'm.report'; static const textToSpeechRule = "p.rule.text_to_speech"; - static const activityResponse = "pangea.activity_res"; + static const pangeaActivityRes = "pangea.activity_res"; static const acitivtyRequest = "pangea.activity_req"; + static const activityRecord = "pangea.activity_completion"; } diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index ad2a27145..b0f65505f 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer'; import 'dart:math'; @@ -12,6 +13,8 @@ import 'package:fluffychat/pangea/controllers/local_settings.dart'; import 'package:fluffychat/pangea/controllers/message_data_controller.dart'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/permissions_controller.dart'; +import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart'; +import 'package:fluffychat/pangea/controllers/practice_activity_record_controller.dart'; import 'package:fluffychat/pangea/controllers/speech_to_text_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; @@ -53,6 +56,8 @@ class PangeaController { late TextToSpeechController textToSpeech; late SpeechToTextController speechToText; late LanguageDetectionController languageDetection; + late PracticeActivityRecordController activityRecordController; + late PracticeGenerationController practiceGenerationController; ///store Services late PLocalStore pStoreService; @@ -101,6 +106,8 @@ class PangeaController { textToSpeech = TextToSpeechController(this); speechToText = SpeechToTextController(this); languageDetection = LanguageDetectionController(this); + activityRecordController = PracticeActivityRecordController(this); + practiceGenerationController = PracticeGenerationController(); PAuthGaurd.pController = this; } diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart new file mode 100644 index 000000000..403e22d4f --- /dev/null +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:matrix/matrix.dart'; + +/// Represents an item in the completion cache. +class _RequestCacheItem { + PracticeActivityRequest req; + + Future practiceActivityEvent; + + _RequestCacheItem({ + required this.req, + required this.practiceActivityEvent, + }); +} + +/// Controller for handling activity completions. +class PracticeGenerationController { + static final Map _cache = {}; + Timer? _cacheClearTimer; + + PracticeGenerationController() { + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 2); + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } + + void dispose() { + _cacheClearTimer?.cancel(); + } + + Future _sendAndPackageEvent( + PracticeActivityModel model, + PangeaMessageEvent pangeaMessageEvent, + ) async { + final Event? activityEvent = await pangeaMessageEvent.room.sendPangeaEvent( + content: model.toJson(), + parentEventId: pangeaMessageEvent.eventId, + type: PangeaEventTypes.pangeaActivityRes, + ); + + if (activityEvent == null) { + return null; + } + + return PracticeActivityEvent( + event: activityEvent, + timeline: pangeaMessageEvent.timeline, + ); + } + + Future getPracticeActivity( + PracticeActivityRequest req, + PangeaMessageEvent event, + ) async { + final int cacheKey = req.hashCode; + + if (_cache.containsKey(cacheKey)) { + return _cache[cacheKey]!.practiceActivityEvent; + } else { + //TODO - send request to server/bot, either via API or via event of type pangeaActivityReq + // for now, just make and send the event from the client + final Future eventFuture = + _sendAndPackageEvent(dummyModel(event), event); + + _cache[cacheKey] = + _RequestCacheItem(req: req, practiceActivityEvent: eventFuture); + + return _cache[cacheKey]!.practiceActivityEvent; + } + } + + PracticeActivityModel dummyModel(PangeaMessageEvent event) => + PracticeActivityModel( + tgtConstructs: [ + ConstructIdentifier(lemma: "be", type: ConstructType.vocab.string), + ], + activityType: ActivityType.multipleChoice, + langCode: event.messageDisplayLangCode, + msgId: event.eventId, + multipleChoice: MultipleChoice( + question: "What is a synonym for 'happy'?", + choices: ["sad", "angry", "joyful", "tired"], + correctAnswer: "joyful", + ), + ); +} diff --git a/lib/pangea/controllers/practice_activity_record_controller.dart b/lib/pangea/controllers/practice_activity_record_controller.dart new file mode 100644 index 000000000..b075fa553 --- /dev/null +++ b/lib/pangea/controllers/practice_activity_record_controller.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +/// Represents an item in the completion cache. +class _RecordCacheItem { + PracticeActivityRecordModel data; + + Future recordEvent; + + _RecordCacheItem({required this.data, required this.recordEvent}); +} + +/// Controller for handling activity completions. +class PracticeActivityRecordController { + static final Map _cache = {}; + late final PangeaController _pangeaController; + Timer? _cacheClearTimer; + + PracticeActivityRecordController(this._pangeaController) { + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 2); + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } + + void dispose() { + _cacheClearTimer?.cancel(); + } + + /// Sends a practice activity record to the server and returns the corresponding event. + /// + /// The [recordModel] parameter is the model representing the practice activity record. + /// The [practiceActivityEvent] parameter is the event associated with the practice activity. + /// Note that the system will send a new event if the model has changed in any way ie it is + /// a new completion of the practice activity. However, it will cache previous sends to ensure + /// that opening and closing of the widget does not result in multiple sends of the same data. + /// It allows checks the data to make sure that it contains responses to the practice activity + /// and does not represent a blank record with no actual completion to be saved. + /// + /// Returns a [Future] that completes with the corresponding [Event] object. + Future send( + PracticeActivityRecordModel recordModel, + PracticeActivityEvent practiceActivityEvent, + ) async { + final int cacheKey = recordModel.hashCode; + + if (recordModel.responses.isEmpty) { + return null; + } + + if (_cache.containsKey(cacheKey)) { + return _cache[cacheKey]!.recordEvent; + } else { + final Future eventFuture = practiceActivityEvent.event.room + .sendPangeaEvent( + content: recordModel.toJson(), + parentEventId: practiceActivityEvent.event.eventId, + type: PangeaEventTypes.activityRecord, + ) + .catchError((e) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: e, + s: StackTrace.current, + data: { + 'recordModel': recordModel.toJson(), + 'practiceActivityEvent': practiceActivityEvent.event.toJson(), + }, + ); + return null; + }); + + _cache[cacheKey] = + _RecordCacheItem(data: recordModel, recordEvent: eventFuture); + + return _cache[cacheKey]!.recordEvent; + } + } +} diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart index dcc3a8bec..17e14ed86 100644 --- a/lib/pangea/extensions/pangea_event_extension.dart +++ b/lib/pangea/extensions/pangea_event_extension.dart @@ -2,6 +2,8 @@ import 'dart:developer'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:flutter/foundation.dart'; @@ -26,9 +28,12 @@ extension PangeaEvent on Event { return PangeaRepresentation.fromJson(json) as V; case PangeaEventTypes.choreoRecord: return ChoreoRecord.fromJson(json) as V; - case PangeaEventTypes.activityResponse: - return PangeaMessageTokens.fromJson(json) as V; + case PangeaEventTypes.pangeaActivityRes: + return PracticeActivityModel.fromJson(json) as V; + case PangeaEventTypes.activityRecord: + return PracticeActivityRecordModel.fromJson(json) as V; default: + debugger(when: kDebugMode); throw Exception("$type events do not have pangea content"); } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_audio_events.dart b/lib/pangea/matrix_event_wrappers/pangea_audio_events.dart deleted file mode 100644 index 3583d021e..000000000 --- a/lib/pangea/matrix_event_wrappers/pangea_audio_events.dart +++ /dev/null @@ -1,9 +0,0 @@ -// relates to a pangea representation event -// the matrix even fits the form of a regular matrix audio event -// but with something to distinguish it as a pangea audio event - -import 'package:matrix/matrix.dart'; - -class PangeaAudioEvent { - Event? _event; -} diff --git a/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart b/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart index 47a6b688f..a6a79fd4f 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; @@ -23,7 +25,7 @@ class ChoreoEvent { _content ??= event.getPangeaContent(); return _content; } catch (err, s) { - if (kDebugMode) rethrow; + debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: s); return null; } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index c874dc93c..66c81bb47 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -4,15 +4,12 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; @@ -576,12 +573,35 @@ class PangeaMessageEvent { _event.messageType != PangeaEventTypes.report && _event.messageType == MessageTypes.Text; + // this is just showActivityIcon now but will include + // logic for showing + bool get showMessageButtons => showActivityIcon; + + /// Returns a boolean value indicating whether to show an activity icon for this message event. + /// + /// The [showActivityIcon] getter checks if the [l2Code] is null, and if so, returns false. + /// Otherwise, it retrieves a list of [PracticeActivityEvent] objects using the [practiceActivities] function + /// with the [l2Code] as an argument. + /// If the list is empty, it returns false. + /// Otherwise, it checks if every activity in the list is complete using the [isComplete] property. + /// If any activity is not complete, it returns true, indicating that the activity icon should be shown. + /// Otherwise, it returns false. + bool get showActivityIcon { + if (l2Code == null) return false; + final List activities = practiceActivities(l2Code!); + + if (activities.isEmpty) return false; + + return !activities.every((activity) => activity.isComplete); + } + + String? get l2Code => MatrixState.pangeaController.languageController + .activeL2Code(roomID: room.id); + String get messageDisplayLangCode { final bool immersionMode = MatrixState .pangeaController.permissionsController .isToolEnabled(ToolSetting.immersionMode, room); - final String? l2Code = MatrixState.pangeaController.languageController - .activeL2Code(roomID: room.id); final String? originalLangCode = (originalWritten ?? originalSent)?.langCode; @@ -608,47 +628,34 @@ class PangeaMessageEvent { List get _practiceActivityEvents => _latestEdit .aggregatedEvents( timeline, - PangeaEventTypes.activityResponse, + PangeaEventTypes.pangeaActivityRes, ) .map( (e) => PracticeActivityEvent( + timeline: timeline, event: e, ), ) .toList(); - List activities(String langCode) { - // final List practiceActivityEvents = _practiceActivityEvents; - - // final List activities = _practiceActivityEvents - // .map( - // (e) => PracticeActivityModel.fromJson( - // e.event.content, - // ), - // ) - // .where( - // (element) => element.langCode == langCode, - // ) - // .toList(); - - // return activities; - - // for now, return a hard-coded activity - final PracticeActivityModel activityModel = PracticeActivityModel( - tgtConstructs: [ - ConstructIdentifier(lemma: "be", type: ConstructType.vocab.string), - ], - activityType: ActivityType.multipleChoice, - langCode: langCode, - msgId: _event.eventId, - multipleChoice: MultipleChoice( - question: "What is a synonym for 'happy'?", - choices: ["sad", "angry", "joyful", "tired"], - correctAnswer: "joyful", - ), - ); + bool get hasActivities { + try { + final String? l2code = MatrixState.pangeaController.languageController + .activeL2Code(roomID: room.id); - return [activityModel]; + if (l2code == null) return false; + + return practiceActivities(l2code).isNotEmpty; + } catch (e, s) { + ErrorHandler.logError(e: e, s: s); + return false; + } + } + + List practiceActivities(String langCode) { + return _practiceActivityEvents + .where((ev) => ev.practiceActivity.langCode == langCode) + .toList(); } // List get activities => diff --git a/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart b/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart index 0c138c637..f617b8dae 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart @@ -1,6 +1,9 @@ +import 'dart:developer'; + import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; import '../constants/pangea_event_types.dart'; @@ -22,6 +25,7 @@ class TokensEvent { _content ??= event.getPangeaContent(); return _content!; } catch (err, s) { + debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: s); return null; } diff --git a/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart b/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart new file mode 100644 index 000000000..d4b9cde23 --- /dev/null +++ b/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart @@ -0,0 +1,24 @@ +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; + +class PracticeActivityRecordEvent { + Event event; + + PracticeActivityRecordModel? _content; + + PracticeActivityRecordEvent({required this.event}) { + if (event.type != PangeaEventTypes.activityRecord) { + throw Exception( + "${event.type} should not be used to make a PracticeActivityRecordEvent", + ); + } + } + + PracticeActivityRecordModel? get record { + _content ??= event.getPangeaContent(); + return _content!; + } +} diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart index 3e5fa5f16..c5f35be91 100644 --- a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart +++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart @@ -1,29 +1,67 @@ +import 'dart:developer'; + import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.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'; import '../constants/pangea_event_types.dart'; class PracticeActivityEvent { Event event; + Timeline? timeline; PracticeActivityModel? _content; - PracticeActivityEvent({required this.event}) { - if (event.type != PangeaEventTypes.activityResponse) { + PracticeActivityEvent({ + required this.event, + required this.timeline, + content, + }) { + if (content != null) { + if (!kDebugMode) { + throw Exception( + "content should not be set on product, just a dev placeholder", + ); + } else { + _content = content; + } + } + if (event.type != PangeaEventTypes.pangeaActivityRes) { throw Exception( "${event.type} should not be used to make a PracticeActivityEvent", ); } } - PracticeActivityModel? get practiceActivity { - try { - _content ??= event.getPangeaContent(); - return _content!; - } catch (err, s) { - ErrorHandler.logError(e: err, s: s); - return null; + PracticeActivityModel get practiceActivity { + _content ??= event.getPangeaContent(); + return _content!; + } + + //in aggregatedEvents for the event, find all practiceActivityRecordEvents whose sender matches the client's userId + List get allRecords { + if (timeline == null) { + debugger(when: kDebugMode); + return []; } + final List records = event + .aggregatedEvents(timeline!, PangeaEventTypes.activityRecord) + .toList(); + + return records + .map((event) => PracticeActivityRecordEvent(event: event)) + .toList(); } + + List get userRecords => allRecords + .where( + (recordEvent) => + recordEvent.event.senderId == recordEvent.event.room.client.userID, + ) + .toList(); + + /// Checks if there are any user records in the list for this activity, + /// and, if so, then the activity is complete + bool get isComplete => userRecords.isNotEmpty; } diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart index a0007e754..0cd6aac05 100644 --- a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart @@ -1,3 +1,6 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; + class MultipleChoice { final String question; final List choices; @@ -9,10 +12,15 @@ class MultipleChoice { required this.correctAnswer, }); + bool isCorrect(int index) => index == correctAnswerIndex; + bool get isValidQuestion => choices.contains(correctAnswer); int get correctAnswerIndex => choices.indexOf(correctAnswer); + Color choiceColor(int index) => + index == correctAnswerIndex ? AppConfig.success : AppConfig.warning; + factory MultipleChoice.fromJson(Map json) { return MultipleChoice( question: json['question'] as String, @@ -29,34 +37,3 @@ class MultipleChoice { }; } } - -// record the options that the user selected -// note that this is not the same as the correct answer -// the user might have selected multiple options before -// finding the answer -class MultipleChoiceActivityCompletionRecord { - final String question; - List selectedOptions; - - MultipleChoiceActivityCompletionRecord({ - required this.question, - this.selectedOptions = const [], - }); - - factory MultipleChoiceActivityCompletionRecord.fromJson( - Map json, - ) { - return MultipleChoiceActivityCompletionRecord( - question: json['question'] as String, - selectedOptions: - (json['selected_options'] as List).map((e) => e as String).toList(), - ); - } - - Map toJson() { - return { - 'question': question, - 'selected_options': selectedOptions, - }; - } -} 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 10aaefa87..ebfd68f37 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -23,15 +23,16 @@ class ConstructIdentifier { enum ActivityType { multipleChoice, freeResponse, listening, speaking } -class MessageInfo { +class CandidateMessage { final String msgId; final String roomId; final String text; - MessageInfo({required this.msgId, required this.roomId, required this.text}); + CandidateMessage( + {required this.msgId, required this.roomId, required this.text}); - factory MessageInfo.fromJson(Map json) { - return MessageInfo( + factory CandidateMessage.fromJson(Map json) { + return CandidateMessage( msgId: json['msg_id'] as String, roomId: json['room_id'] as String, text: json['text'] as String, @@ -47,31 +48,46 @@ class MessageInfo { } } -class ActivityRequest { - final String mode; +enum PracticeActivityMode { focus, srs } + +extension on PracticeActivityMode { + String get value { + switch (this) { + case PracticeActivityMode.focus: + return 'focus'; + case PracticeActivityMode.srs: + return 'srs'; + } + } +} + +class PracticeActivityRequest { + final PracticeActivityMode? mode; final List? targetConstructs; - final List? candidateMessages; + final List? candidateMessages; final List? userIds; final ActivityType? activityType; - final int numActivities; + final int? numActivities; - ActivityRequest({ - required this.mode, + PracticeActivityRequest({ + this.mode, this.targetConstructs, this.candidateMessages, this.userIds, this.activityType, - this.numActivities = 10, + this.numActivities, }); - factory ActivityRequest.fromJson(Map json) { - return ActivityRequest( - mode: json['mode'] as String, + factory PracticeActivityRequest.fromJson(Map json) { + return PracticeActivityRequest( + mode: PracticeActivityMode.values.firstWhere( + (e) => e.value == json['mode'], + ), targetConstructs: (json['target_constructs'] as List?) ?.map((e) => ConstructIdentifier.fromJson(e as Map)) .toList(), candidateMessages: (json['candidate_msgs'] as List) - .map((e) => MessageInfo.fromJson(e as Map)) + .map((e) => CandidateMessage.fromJson(e as Map)) .toList(), userIds: (json['user_ids'] as List?)?.map((e) => e as String).toList(), activityType: ActivityType.values.firstWhere( @@ -83,7 +99,7 @@ class ActivityRequest { Map toJson() { return { - 'mode': mode, + 'mode': mode?.value, 'target_constructs': targetConstructs?.map((e) => e.toJson()).toList(), 'candidate_msgs': candidateMessages?.map((e) => e.toJson()).toList(), 'user_ids': userIds, @@ -91,6 +107,30 @@ class ActivityRequest { 'num_activities': numActivities, }; } + + // override operator == and hashCode + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PracticeActivityRequest && + other.mode == mode && + other.targetConstructs == targetConstructs && + other.candidateMessages == candidateMessages && + other.userIds == userIds && + other.activityType == activityType && + other.numActivities == numActivities; + } + + @override + int get hashCode { + return mode.hashCode ^ + targetConstructs.hashCode ^ + candidateMessages.hashCode ^ + userIds.hashCode ^ + activityType.hashCode ^ + numActivities.hashCode; + } } class FreeResponse { diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart new file mode 100644 index 000000000..7aa7f6b88 --- /dev/null +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -0,0 +1,127 @@ +// record the options that the user selected +// note that this is not the same as the correct answer +// the user might have selected multiple options before +// finding the answer +import 'dart:developer'; +import 'dart:typed_data'; + +class PracticeActivityRecordModel { + final String? question; + late List responses; + + PracticeActivityRecordModel({ + required this.question, + List? responses, + }) { + if (responses == null) { + this.responses = List.empty(growable: true); + } else { + this.responses = responses; + } + } + + factory PracticeActivityRecordModel.fromJson( + Map json, + ) { + return PracticeActivityRecordModel( + question: json['question'] as String, + responses: (json['responses'] as List) + .map((e) => ActivityResponse.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'question': question, + 'responses': responses.map((e) => e.toJson()).toList(), + }; + } + + void addResponse({ + String? text, + Uint8List? audioBytes, + Uint8List? imageBytes, + }) { + try { + responses.add( + ActivityResponse( + text: text, + audioBytes: audioBytes, + imageBytes: imageBytes, + timestamp: DateTime.now(), + ), + ); + } catch (e) { + debugger(); + } + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PracticeActivityRecordModel && + other.question == question && + other.responses.length == responses.length && + List.generate( + responses.length, + (index) => responses[index] == other.responses[index], + ).every((element) => element); + } + + @override + int get hashCode => question.hashCode ^ responses.hashCode; +} + +class ActivityResponse { + // the user's response + // has nullable string, nullable audio bytes, nullable image bytes, and timestamp + final String? text; + final Uint8List? audioBytes; + final Uint8List? imageBytes; + final DateTime timestamp; + + ActivityResponse({ + this.text, + this.audioBytes, + this.imageBytes, + required this.timestamp, + }); + + factory ActivityResponse.fromJson(Map json) { + return ActivityResponse( + text: json['text'] as String?, + audioBytes: json['audio'] as Uint8List?, + imageBytes: json['image'] as Uint8List?, + timestamp: DateTime.parse(json['timestamp'] as String), + ); + } + + Map toJson() { + return { + 'text': text, + 'audio': audioBytes, + 'image': imageBytes, + 'timestamp': timestamp.toIso8601String(), + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ActivityResponse && + other.text == text && + other.audioBytes == audioBytes && + other.imageBytes == imageBytes && + other.timestamp == timestamp; + } + + @override + int get hashCode => + text.hashCode ^ + audioBytes.hashCode ^ + imageBytes.hashCode ^ + timestamp.hashCode; +} diff --git a/lib/pangea/widgets/chat/message_buttons.dart b/lib/pangea/widgets/chat/message_buttons.dart new file mode 100644 index 000000000..f7748675f --- /dev/null +++ b/lib/pangea/widgets/chat/message_buttons.dart @@ -0,0 +1,96 @@ +import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:flutter/material.dart'; + +class MessageButtons extends StatelessWidget { + final ToolbarDisplayController? toolbarController; + + const MessageButtons({ + super.key, + this.toolbarController, + }); + + void showActivity(BuildContext context) { + toolbarController?.showToolbar( + context, + mode: MessageMode.practiceActivity, + ); + } + + @override + Widget build(BuildContext context) { + if (toolbarController == null) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Row( + children: [ + HoverIconButton( + icon: MessageMode.practiceActivity.icon, + onTap: () => showActivity(context), + primaryColor: Theme.of(context).colorScheme.primary, + tooltip: MessageMode.practiceActivity.tooltip(context), + ), + + // Additional buttons can be added here in the future + ], + ), + ); + } +} + +class HoverIconButton extends StatefulWidget { + final IconData icon; + final VoidCallback onTap; + final Color primaryColor; + final String tooltip; + + const HoverIconButton({ + super.key, + required this.icon, + required this.onTap, + required this.primaryColor, + required this.tooltip, + }); + + @override + _HoverIconButtonState createState() => _HoverIconButtonState(); +} + +class _HoverIconButtonState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: widget.tooltip, + child: InkWell( + onTap: widget.onTap, + onHover: (hovering) { + setState(() => _isHovered = hovering); + }, + borderRadius: BorderRadius.circular(100), + child: Container( + decoration: BoxDecoration( + color: _isHovered ? widget.primaryColor : null, + borderRadius: BorderRadius.circular(100), + border: Border.all( + width: 1, + color: widget.primaryColor, + ), + ), + padding: const EdgeInsets.all(2), + child: Icon( + widget.icon, + size: 18, + // when hovered, use themeData to get background color, otherwise use primary + color: _isHovered + ? Theme.of(context).scaffoldBackgroundColor + : widget.primaryColor, + ), + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 523637b37..ff7b73b72 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -277,12 +277,8 @@ class MessageToolbarState extends State { } void showPracticeActivity() { - toolbarContent = PracticeActivityCard( - practiceActivity: widget.pangeaMessageEvent - // @ggurdin - is this the best way to get the l2 language here? - .activities(widget.pangeaMessageEvent.messageDisplayLangCode) - .first, - ); + toolbarContent = + PracticeActivityCard(pangeaMessageEvent: widget.pangeaMessageEvent); } void showImage() {} diff --git a/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart b/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart new file mode 100644 index 000000000..02d7e90cd --- /dev/null +++ b/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart @@ -0,0 +1,60 @@ +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class GeneratePracticeActivityButton extends StatelessWidget { + final PangeaMessageEvent pangeaMessageEvent; + final Function(PracticeActivityEvent?) onActivityGenerated; + + const GeneratePracticeActivityButton({ + super.key, + required this.pangeaMessageEvent, + required this.onActivityGenerated, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () async { + final String? l2Code = MatrixState.pangeaController.languageController + .activeL1Model(roomID: pangeaMessageEvent.room.id) + ?.langCode; + + if (l2Code == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.noLanguagesSet), + ), + ); + return; + } + + final PracticeActivityEvent? practiceActivityEvent = await MatrixState + .pangeaController.practiceGenerationController + .getPracticeActivity( + PracticeActivityRequest( + candidateMessages: [ + CandidateMessage( + msgId: pangeaMessageEvent.eventId, + roomId: pangeaMessageEvent.room.id, + text: + pangeaMessageEvent.representationByLanguage(l2Code)?.text ?? + pangeaMessageEvent.body, + ), + ], + userIds: pangeaMessageEvent.room.client.userID != null + ? [pangeaMessageEvent.room.client.userID!] + : null, + ), + pangeaMessageEvent, + ); + + onActivityGenerated(practiceActivityEvent); + }, + child: Text(L10n.of(context)!.practice), + ); + } +} diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart b/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart index c5bb5dbfa..d69627fcb 100644 --- a/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart @@ -1,15 +1,17 @@ -//stateful widget that displays a card with a practice activity - -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity_card/generate_practice_activity.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_content.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class PracticeActivityCard extends StatefulWidget { - final PracticeActivityModel practiceActivity; + final PangeaMessageEvent pangeaMessageEvent; const PracticeActivityCard({ super.key, - required this.practiceActivity, + required this.pangeaMessageEvent, }); @override @@ -17,22 +19,49 @@ class PracticeActivityCard extends StatefulWidget { MessagePracticeActivityCardState(); } -//parameters for the stateful widget -// practiceActivity: the practice activity to display -// use a switch statement based on the type of the practice activity to display the appropriate content -// just use different widgets for the different types, don't define in this file -// for multiple choice, use the MultipleChoiceActivity widget -// for the rest, just return SizedBox.shrink() for now class MessagePracticeActivityCardState extends State { + PracticeActivityEvent? practiceEvent; + + @override + void initState() { + super.initState(); + loadInitialData(); + } + + void loadInitialData() { + final String? langCode = MatrixState.pangeaController.languageController + .activeL2Model(roomID: widget.pangeaMessageEvent.room.id) + ?.langCode; + + if (langCode == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context)!.noLanguagesSet)), + ); + return; + } + + practiceEvent = + widget.pangeaMessageEvent.practiceActivities(langCode).firstOrNull; + setState(() {}); + } + + void updatePracticeActivity(PracticeActivityEvent? newEvent) { + setState(() { + practiceEvent = newEvent; + }); + } + @override Widget build(BuildContext context) { - switch (widget.practiceActivity.activityType) { - case ActivityType.multipleChoice: - return MultipleChoiceActivity( - practiceActivity: widget.practiceActivity, - ); - default: - return const SizedBox.shrink(); + if (practiceEvent == null) { + return GeneratePracticeActivityButton( + pangeaMessageEvent: widget.pangeaMessageEvent, + onActivityGenerated: updatePracticeActivity, + ); } + return PracticeActivityContent( + practiceEvent: practiceEvent!, + pangeaMessageEvent: widget.pangeaMessageEvent, + ); } } diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart new file mode 100644 index 000000000..fc0119012 --- /dev/null +++ b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart @@ -0,0 +1,141 @@ +import 'package:collection/collection.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class PracticeActivityContent extends StatefulWidget { + final PracticeActivityEvent practiceEvent; + final PangeaMessageEvent pangeaMessageEvent; + + const PracticeActivityContent({ + super.key, + required this.practiceEvent, + required this.pangeaMessageEvent, + }); + + @override + MessagePracticeActivityContentState createState() => + MessagePracticeActivityContentState(); +} + +class MessagePracticeActivityContentState + extends State { + int? selectedChoiceIndex; + PracticeActivityRecordModel? recordModel; + bool recordSubmittedThisSession = false; + bool recordSubmittedPreviousSession = false; + + PracticeActivityEvent get practiceEvent => widget.practiceEvent; + + @override + void initState() { + super.initState(); + final PracticeActivityRecordEvent? recordEvent = + widget.practiceEvent.userRecords.firstOrNull; + if (recordEvent?.record == null) { + recordModel = PracticeActivityRecordModel( + question: + widget.practiceEvent.practiceActivity.multipleChoice!.question, + ); + } else { + recordModel = recordEvent!.record; + recordSubmittedPreviousSession = true; + recordSubmittedThisSession = true; + } + } + + void updateChoice(int index) { + setState(() { + selectedChoiceIndex = index; + recordModel!.addResponse( + text: widget + .practiceEvent.practiceActivity.multipleChoice!.choices[index], + ); + }); + } + + Widget get activityWidget { + switch (widget.practiceEvent.practiceActivity.activityType) { + case ActivityType.multipleChoice: + return MultipleChoiceActivity( + card: this, + updateChoice: updateChoice, + isActive: + !recordSubmittedPreviousSession && !recordSubmittedThisSession, + ); + default: + return const SizedBox.shrink(); + } + } + + void sendRecord() { + MatrixState.pangeaController.activityRecordController + .send( + recordModel!, + widget.practiceEvent, + ) + .catchError((error) { + ErrorHandler.logError( + e: error, + s: StackTrace.current, + data: { + 'recordModel': recordModel?.toJson(), + 'practiceEvent': widget.practiceEvent.event.toJson(), + }, + ); + return null; + }); + + setState(() { + recordSubmittedThisSession = true; + }); + } + + @override + Widget build(BuildContext context) { + debugPrint( + "MessagePracticeActivityContentState.build with selectedChoiceIndex: $selectedChoiceIndex", + ); + return Column( + children: [ + activityWidget, + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Opacity( + opacity: selectedChoiceIndex != null && + !recordSubmittedThisSession && + !recordSubmittedPreviousSession + ? 1.0 + : 0.5, + child: TextButton( + onPressed: () { + if (recordSubmittedThisSession || + recordSubmittedPreviousSession) { + return; + } + selectedChoiceIndex != null ? sendRecord() : null; + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + AppConfig.primaryColor, + ), + ), + child: Text(L10n.of(context)!.submit), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart index f8bec5436..f74dc03ce 100644 --- a/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart @@ -1,49 +1,39 @@ -// stateful widget that displays a card with a practice activity of type multiple choice - import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_content.dart'; import 'package:flutter/material.dart'; -class MultipleChoiceActivity extends StatefulWidget { - final PracticeActivityModel practiceActivity; +class MultipleChoiceActivity extends StatelessWidget { + final MessagePracticeActivityContentState card; + final Function(int) updateChoice; + final bool isActive; const MultipleChoiceActivity({ super.key, - required this.practiceActivity, + required this.card, + required this.updateChoice, + required this.isActive, }); - @override - MultipleChoiceActivityState createState() => MultipleChoiceActivityState(); -} - -//parameters for the stateful widget -// practiceActivity: the practice activity to display -// show the question text and choices -// use the ChoiceArray widget to display the choices -class MultipleChoiceActivityState extends State { - int? selectedChoiceIndex; + PracticeActivityEvent get practiceEvent => card.practiceEvent; - late MultipleChoiceActivityCompletionRecord? completionRecord; + int? get selectedChoiceIndex => card.selectedChoiceIndex; - @override - initState() { - super.initState(); - selectedChoiceIndex = null; - completionRecord = MultipleChoiceActivityCompletionRecord( - question: widget.practiceActivity.multipleChoice!.question, - ); - } + bool get submitted => card.recordSubmittedThisSession; @override Widget build(BuildContext context) { + final PracticeActivityModel practiceActivity = + practiceEvent.practiceActivity; + return Container( padding: const EdgeInsets.all(8), child: Column( children: [ Text( - widget.practiceActivity.multipleChoice!.question, + practiceActivity.multipleChoice!.question, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -53,26 +43,24 @@ class MultipleChoiceActivityState extends State { ChoicesArray( isLoading: false, uniqueKeyForLayerLink: (index) => "multiple_choice_$index", - onLongPress: null, - onPressed: (index) { - selectedChoiceIndex = index; - completionRecord!.selectedOptions - .add(widget.practiceActivity.multipleChoice!.choices[index]); - setState(() {}); - }, originalSpan: "placeholder", + onPressed: updateChoice, selectedChoiceIndex: selectedChoiceIndex, - choices: widget.practiceActivity.multipleChoice!.choices + choices: practiceActivity.multipleChoice!.choices .mapIndexed( - (int index, String value) => Choice( + (index, value) => Choice( text: value, - color: null, - isGold: - widget.practiceActivity.multipleChoice!.correctAnswer == - value, + color: (selectedChoiceIndex == index || + practiceActivity.multipleChoice! + .isCorrect(index)) && + submitted + ? practiceActivity.multipleChoice!.choiceColor(index) + : null, + isGold: practiceActivity.multipleChoice!.isCorrect(index), ), ) .toList(), + isActive: isActive, ), ], ), diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart index 4d09b506e..51082a7e6 100644 --- a/lib/pangea/widgets/user_settings/p_language_dialog.dart +++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart @@ -98,7 +98,6 @@ pLanguageDialog(BuildContext parentContext, Function callback) async { Navigator.pop(context); } catch (err, s) { debugger(when: kDebugMode); - //PTODO-Lala add standard error message ErrorHandler.logError(e: err, s: s); rethrow; } finally { diff --git a/needed-translations.txt b/needed-translations.txt index 6a6224756..50198bb38 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -840,7 +840,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "be": [ @@ -2279,7 +2280,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "bn": [ @@ -3180,7 +3182,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "bo": [ @@ -4081,7 +4084,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ca": [ @@ -4982,7 +4986,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "cs": [ @@ -5883,7 +5888,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "de": [ @@ -6731,7 +6737,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "el": [ @@ -7632,7 +7639,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "eo": [ @@ -8533,11 +8541,13 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "es": [ - "practice" + "practice", + "noLanguagesSet" ], "et": [ @@ -9381,7 +9391,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "eu": [ @@ -10225,7 +10236,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "fa": [ @@ -11126,7 +11138,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "fi": [ @@ -12027,7 +12040,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "fr": [ @@ -12928,7 +12942,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ga": [ @@ -13829,7 +13844,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "gl": [ @@ -14673,7 +14689,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "he": [ @@ -15574,7 +15591,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "hi": [ @@ -16475,7 +16493,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "hr": [ @@ -17363,7 +17382,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "hu": [ @@ -18264,7 +18284,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ia": [ @@ -19689,7 +19710,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "id": [ @@ -20590,7 +20612,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ie": [ @@ -21491,7 +21514,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "it": [ @@ -22377,7 +22401,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ja": [ @@ -23278,7 +23303,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ko": [ @@ -24179,7 +24205,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "lt": [ @@ -25080,7 +25107,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "lv": [ @@ -25981,7 +26009,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "nb": [ @@ -26882,7 +26911,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "nl": [ @@ -27783,7 +27813,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "pl": [ @@ -28684,7 +28715,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "pt": [ @@ -29585,7 +29617,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "pt_BR": [ @@ -30455,7 +30488,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "pt_PT": [ @@ -31356,7 +31390,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ro": [ @@ -32257,7 +32292,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ru": [ @@ -33101,7 +33137,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "sk": [ @@ -34002,7 +34039,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "sl": [ @@ -34903,7 +34941,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "sr": [ @@ -35804,7 +35843,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "sv": [ @@ -36670,7 +36710,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "ta": [ @@ -37571,7 +37612,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "th": [ @@ -38472,7 +38514,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "tr": [ @@ -39358,7 +39401,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "uk": [ @@ -40202,7 +40246,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "vi": [ @@ -41103,7 +41148,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "zh": [ @@ -41947,7 +41993,8 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ], "zh_Hant": [ @@ -42848,6 +42895,7 @@ "roomDataMissing", "updatePhoneOS", "wordsPerMinute", - "practice" + "practice", + "noLanguagesSet" ] } diff --git a/pangea_packages/fcm_shared_isolate/pubspec.lock b/pangea_packages/fcm_shared_isolate/pubspec.lock index e18c1dba1..4d8bacbed 100644 --- a/pangea_packages/fcm_shared_isolate/pubspec.lock +++ b/pangea_packages/fcm_shared_isolate/pubspec.lock @@ -128,38 +128,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" pedantic: dependency: "direct dev" description: @@ -225,10 +249,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" vector_math: dependency: transitive description: @@ -237,14 +261,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - web: + vm_service: dependency: transitive description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "14.2.1" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" - flutter: ">=1.20.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" From 5d5b35b4eb93aac536b4f7591f42e0749a107680 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Thu, 13 Jun 2024 12:43:29 -0400 Subject: [PATCH 03/15] switch to enum for construct type --- .../practice_activity_generation_controller.dart | 2 +- .../practice_activity_model.dart | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 403e22d4f..fdd6da4fa 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -87,7 +87,7 @@ class PracticeGenerationController { PracticeActivityModel dummyModel(PangeaMessageEvent event) => PracticeActivityModel( tgtConstructs: [ - ConstructIdentifier(lemma: "be", type: ConstructType.vocab.string), + ConstructIdentifier(lemma: "be", type: ConstructType.vocab), ], activityType: ActivityType.multipleChoice, langCode: event.messageDisplayLangCode, 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 ebfd68f37..909406430 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -1,22 +1,25 @@ +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; class ConstructIdentifier { final String lemma; - final String type; + final ConstructType type; ConstructIdentifier({required this.lemma, required this.type}); factory ConstructIdentifier.fromJson(Map json) { return ConstructIdentifier( lemma: json['lemma'] as String, - type: json['type'] as String, + type: ConstructType.values.firstWhere( + (e) => e.string == json['type'], + ), ); } Map toJson() { return { 'lemma': lemma, - 'type': type, + 'type': type.string, }; } } @@ -28,8 +31,11 @@ class CandidateMessage { final String roomId; final String text; - CandidateMessage( - {required this.msgId, required this.roomId, required this.text}); + CandidateMessage({ + required this.msgId, + required this.roomId, + required this.text, + }); factory CandidateMessage.fromJson(Map json) { return CandidateMessage( From d6a56cbd43914aa83e1279d7d5fefd376c9f899e Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sat, 22 Jun 2024 13:26:12 -0400 Subject: [PATCH 04/15] successfully received/completed bot-generated practice activity --- ...actice_activity_generation_controller.dart | 5 ++- lib/pangea/enum/activity_type_enum.dart | 16 ++++++++ .../pangea_message_event.dart | 14 +++++-- .../multiple_choice_activity_model.dart | 12 +++--- .../practice_activity_model.dart | 39 +++++++++++------- .../message_practice_activity_content.dart | 4 +- pubspec.lock | 40 +++++++++---------- 7 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 lib/pangea/enum/activity_type_enum.dart diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index fdd6da4fa..29047d0c4 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; @@ -89,13 +90,13 @@ class PracticeGenerationController { tgtConstructs: [ ConstructIdentifier(lemma: "be", type: ConstructType.vocab), ], - activityType: ActivityType.multipleChoice, + activityType: ActivityTypeEnum.multipleChoice, langCode: event.messageDisplayLangCode, msgId: event.eventId, multipleChoice: MultipleChoice( question: "What is a synonym for 'happy'?", choices: ["sad", "angry", "joyful", "tired"], - correctAnswer: "joyful", + answer: "joyful", ), ); } diff --git a/lib/pangea/enum/activity_type_enum.dart b/lib/pangea/enum/activity_type_enum.dart new file mode 100644 index 000000000..d429aa038 --- /dev/null +++ b/lib/pangea/enum/activity_type_enum.dart @@ -0,0 +1,16 @@ +enum ActivityTypeEnum { multipleChoice, freeResponse, listening, speaking } + +extension ActivityTypeExtension on ActivityTypeEnum { + String get string { + switch (this) { + case ActivityTypeEnum.multipleChoice: + return 'multiple_choice'; + case ActivityTypeEnum.freeResponse: + return 'free_response'; + case ActivityTypeEnum.listening: + return 'listening'; + case ActivityTypeEnum.speaking: + return 'speaking'; + } + } +} diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 66c81bb47..32b8c1a29 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; @@ -15,6 +16,7 @@ import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -653,9 +655,15 @@ class PangeaMessageEvent { } List practiceActivities(String langCode) { - return _practiceActivityEvents - .where((ev) => ev.practiceActivity.langCode == langCode) - .toList(); + try { + return _practiceActivityEvents + .where((ev) => ev.practiceActivity.langCode == langCode) + .toList(); + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s, data: event.toJson()); + return []; + } } // List get activities => diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart index 0cd6aac05..e152c18a2 100644 --- a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart @@ -4,19 +4,19 @@ import 'package:flutter/material.dart'; class MultipleChoice { final String question; final List choices; - final String correctAnswer; + final String answer; MultipleChoice({ required this.question, required this.choices, - required this.correctAnswer, + required this.answer, }); bool isCorrect(int index) => index == correctAnswerIndex; - bool get isValidQuestion => choices.contains(correctAnswer); + bool get isValidQuestion => choices.contains(answer); - int get correctAnswerIndex => choices.indexOf(correctAnswer); + int get correctAnswerIndex => choices.indexOf(answer); Color choiceColor(int index) => index == correctAnswerIndex ? AppConfig.success : AppConfig.warning; @@ -25,7 +25,7 @@ class MultipleChoice { return MultipleChoice( question: json['question'] as String, choices: (json['choices'] as List).map((e) => e as String).toList(), - correctAnswer: json['correct_answer'] as String, + answer: json['answer'] as String, ); } @@ -33,7 +33,7 @@ class MultipleChoice { return { 'question': question, 'choices': choices, - 'correct_answer': correctAnswer, + 'answer': answer, }; } } 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 909406430..ae8455c7f 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -1,5 +1,10 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; class ConstructIdentifier { final String lemma; @@ -8,12 +13,18 @@ class ConstructIdentifier { ConstructIdentifier({required this.lemma, required this.type}); factory ConstructIdentifier.fromJson(Map json) { - return ConstructIdentifier( - lemma: json['lemma'] as String, - type: ConstructType.values.firstWhere( - (e) => e.string == json['type'], - ), - ); + try { + return ConstructIdentifier( + lemma: json['lemma'] as String, + type: ConstructType.values.firstWhere( + (e) => e.string == json['type'], + ), + ); + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s, data: json); + rethrow; + } } Map toJson() { @@ -24,8 +35,6 @@ class ConstructIdentifier { } } -enum ActivityType { multipleChoice, freeResponse, listening, speaking } - class CandidateMessage { final String msgId; final String roomId; @@ -72,7 +81,7 @@ class PracticeActivityRequest { final List? targetConstructs; final List? candidateMessages; final List? userIds; - final ActivityType? activityType; + final ActivityTypeEnum? activityType; final int? numActivities; PracticeActivityRequest({ @@ -96,7 +105,7 @@ class PracticeActivityRequest { .map((e) => CandidateMessage.fromJson(e as Map)) .toList(), userIds: (json['user_ids'] as List?)?.map((e) => e as String).toList(), - activityType: ActivityType.values.firstWhere( + activityType: ActivityTypeEnum.values.firstWhere( (e) => e.toString().split('.').last == json['activity_type'], ), numActivities: json['num_activities'] as int, @@ -210,7 +219,7 @@ class PracticeActivityModel { final List tgtConstructs; final String langCode; final String msgId; - final ActivityType activityType; + final ActivityTypeEnum activityType; final MultipleChoice? multipleChoice; final Listening? listening; final Speaking? speaking; @@ -234,8 +243,8 @@ class PracticeActivityModel { .toList(), langCode: json['lang_code'] as String, msgId: json['msg_id'] as String, - activityType: ActivityType.values.firstWhere( - (e) => e.toString().split('.').last == json['activity_type'], + activityType: ActivityTypeEnum.values.firstWhere( + (e) => e.string == json['activity_type'], ), multipleChoice: json['multiple_choice'] != null ? MultipleChoice.fromJson( @@ -249,7 +258,9 @@ class PracticeActivityModel { ? Speaking.fromJson(json['speaking'] as Map) : null, freeResponse: json['free_response'] != null - ? FreeResponse.fromJson(json['free_response'] as Map) + ? FreeResponse.fromJson( + json['free_response'] as Map, + ) : null, ); } diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart index fc0119012..8f234bc8d 100644 --- a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart @@ -1,9 +1,9 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart'; @@ -64,7 +64,7 @@ class MessagePracticeActivityContentState Widget get activityWidget { switch (widget.practiceEvent.practiceActivity.activityType) { - case ActivityType.multipleChoice: + case ActivityTypeEnum.multipleChoice: return MultipleChoiceActivity( card: this, updateChoice: updateChoice, diff --git a/pubspec.lock b/pubspec.lock index 8932ea070..cdc07b8fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1184,10 +1184,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -1200,10 +1200,10 @@ packages: dependency: transitive description: name: jiffy - sha256: cc1d4b75016a9156c29b5d61f0c9176c3e0fb0580cc5a0e0422b5d2cab3fbfff + sha256: "3497caaa36d36a29033e66803c9739ce6bccbc7e241ca46070f76ee9e6f6eb0c" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.1" js: dependency: transitive description: @@ -1281,26 +1281,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" license_checker: dependency: "direct dev" description: @@ -1425,10 +1425,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mgrs_dart: dependency: transitive description: @@ -2254,26 +2254,26 @@ packages: dependency: transitive description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" timezone: dependency: transitive description: @@ -2566,10 +2566,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" wakelock_plus: dependency: "direct main" description: From 15f493709f78b78c6be105de80d25ad6febee924 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 23 Jun 2024 14:09:26 -0400 Subject: [PATCH 05/15] client set up to recieve and display multiple choice questions with some display tweaks --- .../matrix_event_wrappers/pangea_message_event.dart | 6 +++--- .../practice_activity_record_model.dart | 10 ++++++++++ lib/pangea/widgets/chat/message_toolbar.dart | 6 ++++++ .../generate_practice_activity.dart | 1 + .../message_practice_activity_content.dart | 1 + 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 32b8c1a29..c9c05aca1 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -577,18 +577,18 @@ class PangeaMessageEvent { // this is just showActivityIcon now but will include // logic for showing - bool get showMessageButtons => showActivityIcon; + bool get showMessageButtons => hasUncompletedActivity; /// Returns a boolean value indicating whether to show an activity icon for this message event. /// - /// The [showActivityIcon] getter checks if the [l2Code] is null, and if so, returns false. + /// The [hasUncompletedActivity] getter checks if the [l2Code] is null, and if so, returns false. /// Otherwise, it retrieves a list of [PracticeActivityEvent] objects using the [practiceActivities] function /// with the [l2Code] as an argument. /// If the list is empty, it returns false. /// Otherwise, it checks if every activity in the list is complete using the [isComplete] property. /// If any activity is not complete, it returns true, indicating that the activity icon should be shown. /// Otherwise, it returns false. - bool get showActivityIcon { + bool get hasUncompletedActivity { if (l2Code == null) return false; final List activities = practiceActivities(l2Code!); diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart index 7aa7f6b88..93ec8e478 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -38,6 +38,16 @@ class PracticeActivityRecordModel { }; } + /// get the latest response index according to the response timeStamp + /// sort the responses by timestamp and get the index of the last response + int? get latestResponseIndex { + if (responses.isEmpty) { + return null; + } + responses.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + return responses.length - 1; + } + void addResponse({ String? text, Uint8List? audioBytes, diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index ff7b73b72..9bb5aacbe 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -189,6 +189,12 @@ class MessageToolbarState extends State { return; } + // if there is an uncompleted activity, then show that + // we don't want the user to user the tools to get the answer :P + if (widget.pangeaMessageEvent.hasUncompletedActivity) { + newMode = MessageMode.practiceActivity; + } + if (mounted) { setState(() { currentMode = newMode; diff --git a/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart b/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart index 02d7e90cd..7d01d8b4c 100644 --- a/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart +++ b/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart @@ -15,6 +15,7 @@ class GeneratePracticeActivityButton extends StatelessWidget { required this.onActivityGenerated, }); + //TODO - probably disable the generation of activities for specific messages @override Widget build(BuildContext context) { return ElevatedButton( diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart index 8f234bc8d..6b7ebf7d9 100644 --- a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart @@ -47,6 +47,7 @@ class MessagePracticeActivityContentState ); } else { recordModel = recordEvent!.record; + selectedChoiceIndex = recordModel!.latestResponseIndex; recordSubmittedPreviousSession = true; recordSubmittedThisSession = true; } From 9c75d4e2d53362e83e2f450b982415c0a25983ef Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Tue, 25 Jun 2024 10:52:16 -0400 Subject: [PATCH 06/15] debugging why certain activities would not show --- .../pangea_message_event.dart | 14 ++++++++++---- lib/pangea/widgets/chat/message_toolbar.dart | 2 +- .../generate_practice_activity.dart | 0 .../multiple_choice_activity.dart | 2 +- .../practice_activity_card.dart} | 12 ++++++++++-- .../practice_activity_content.dart} | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) rename lib/pangea/widgets/{practice_activity_card => practice_activity}/generate_practice_activity.dart (100%) rename lib/pangea/widgets/{practice_activity_card => practice_activity}/multiple_choice_activity.dart (95%) rename lib/pangea/widgets/{practice_activity_card/message_practice_activity_card.dart => practice_activity/practice_activity_card.dart} (83%) rename lib/pangea/widgets/{practice_activity_card/message_practice_activity_content.dart => practice_activity/practice_activity_content.dart} (97%) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index c9c05aca1..2932aae47 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -654,11 +654,17 @@ class PangeaMessageEvent { } } - List practiceActivities(String langCode) { + List practiceActivities(String langCode, + {bool debug = false}) { try { - return _practiceActivityEvents - .where((ev) => ev.practiceActivity.langCode == langCode) - .toList(); + debugger(when: debug); + final List activities = []; + for (final event in _practiceActivityEvents) { + if (event.practiceActivity.langCode == langCode) { + activities.add(event); + } + } + return activities; } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError(e: e, s: s, data: event.toJson()); diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 9bb5aacbe..39729aa7c 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -16,7 +16,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_card.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart b/lib/pangea/widgets/practice_activity/generate_practice_activity.dart similarity index 100% rename from lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart rename to lib/pangea/widgets/practice_activity/generate_practice_activity.dart diff --git a/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart similarity index 95% rename from lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart rename to lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index f74dc03ce..c2861ffe0 100644 --- a/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_content.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart'; import 'package:flutter/material.dart'; class MultipleChoiceActivity extends StatelessWidget { diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart similarity index 83% rename from lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart rename to lib/pangea/widgets/practice_activity/practice_activity_card.dart index d69627fcb..f4312b8fd 100644 --- a/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -1,8 +1,11 @@ +import 'dart:developer'; + import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity_card/generate_practice_activity.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_content.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/generate_practice_activity.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -37,11 +40,16 @@ class MessagePracticeActivityCardState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n.of(context)!.noLanguagesSet)), ); + debugger(when: kDebugMode); return; } practiceEvent = widget.pangeaMessageEvent.practiceActivities(langCode).firstOrNull; + + if (practiceEvent == null) { + debugger(when: kDebugMode); + } setState(() {}); } diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart similarity index 97% rename from lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart rename to lib/pangea/widgets/practice_activity/practice_activity_content.dart index 6b7ebf7d9..d5f387cfd 100644 --- a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -6,7 +6,7 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_recor import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; From f667a35dce8fe4b2778c59358efb8d46cd190694 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Tue, 25 Jun 2024 12:19:27 -0400 Subject: [PATCH 07/15] hide generation button --- assets/l10n/intl_en.arb | 4 +- .../pangea_message_event.dart | 4 +- .../generate_practice_activity.dart | 2 +- .../practice_activity_card.dart | 12 +- needed-translations.txt | 200 +++++++++++++----- 5 files changed, 162 insertions(+), 60 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 4a27fd6da..b83a6ff08 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4055,5 +4055,7 @@ "spaceAnalytics": "Space Analytics", "changeAnalyticsLanguage": "Change Analytics Language", "suggestToSpace": "Suggest this space", - "suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces" + "suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces", + "practice": "Practice", + "noLanguagesSet": "No languages set" } \ No newline at end of file diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index cf3ee492d..870d305a1 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -620,8 +620,8 @@ class PangeaMessageEvent { bool get hasActivities { try { - final String? l2code = MatrixState.pangeaController.languageController - .activeL2Code(roomID: room.id); + final String? l2code = + MatrixState.pangeaController.languageController.activeL2Code(); if (l2code == null) return false; diff --git a/lib/pangea/widgets/practice_activity/generate_practice_activity.dart b/lib/pangea/widgets/practice_activity/generate_practice_activity.dart index 7d01d8b4c..1eae97d63 100644 --- a/lib/pangea/widgets/practice_activity/generate_practice_activity.dart +++ b/lib/pangea/widgets/practice_activity/generate_practice_activity.dart @@ -21,7 +21,7 @@ class GeneratePracticeActivityButton extends StatelessWidget { return ElevatedButton( onPressed: () async { final String? l2Code = MatrixState.pangeaController.languageController - .activeL1Model(roomID: pangeaMessageEvent.room.id) + .activeL1Model() ?.langCode; if (l2Code == null) { diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index f4312b8fd..18713c2c9 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -2,7 +2,6 @@ import 'dart:developer'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity/generate_practice_activity.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -33,7 +32,7 @@ class MessagePracticeActivityCardState extends State { void loadInitialData() { final String? langCode = MatrixState.pangeaController.languageController - .activeL2Model(roomID: widget.pangeaMessageEvent.room.id) + .activeL2Model() ?.langCode; if (langCode == null) { @@ -62,10 +61,11 @@ class MessagePracticeActivityCardState extends State { @override Widget build(BuildContext context) { if (practiceEvent == null) { - return GeneratePracticeActivityButton( - pangeaMessageEvent: widget.pangeaMessageEvent, - onActivityGenerated: updatePracticeActivity, - ); + return const Text('No practice activities found for this message'); + // return GeneratePracticeActivityButton( + // pangeaMessageEvent: widget.pangeaMessageEvent, + // onActivityGenerated: updatePracticeActivity, + // ); } return PracticeActivityContent( practiceEvent: practiceEvent!, diff --git a/needed-translations.txt b/needed-translations.txt index 6dae1ed19..d7704d03e 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -860,7 +860,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "be": [ @@ -2357,7 +2359,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "bn": [ @@ -3850,7 +3854,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "bo": [ @@ -5347,7 +5353,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ca": [ @@ -6246,7 +6254,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "cs": [ @@ -7227,7 +7237,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "de": [ @@ -8091,7 +8103,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "el": [ @@ -9539,7 +9553,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "eo": [ @@ -10685,7 +10701,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "es": [ @@ -10697,7 +10715,9 @@ "addConversationBotButtonRemove", "addConversationBotDialogRemoveConfirmation", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "et": [ @@ -11561,7 +11581,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "eu": [ @@ -12427,7 +12449,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "fa": [ @@ -13430,7 +13454,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "fi": [ @@ -14397,7 +14423,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "fil": [ @@ -15720,7 +15748,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "fr": [ @@ -16722,7 +16752,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ga": [ @@ -17853,7 +17885,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "gl": [ @@ -18717,7 +18751,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "he": [ @@ -19967,7 +20003,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "hi": [ @@ -21457,7 +21495,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "hr": [ @@ -22400,7 +22440,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "hu": [ @@ -23280,7 +23322,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ia": [ @@ -24763,7 +24807,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "id": [ @@ -25633,7 +25679,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ie": [ @@ -26887,7 +26935,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "it": [ @@ -27808,7 +27858,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ja": [ @@ -28840,7 +28892,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ka": [ @@ -30191,7 +30245,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ko": [ @@ -31057,7 +31113,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "lt": [ @@ -32089,7 +32147,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "lv": [ @@ -32961,7 +33021,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "nb": [ @@ -34157,7 +34219,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "nl": [ @@ -35117,7 +35181,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "pl": [ @@ -36086,7 +36152,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "pt": [ @@ -37561,7 +37629,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "pt_BR": [ @@ -38431,7 +38501,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "pt_PT": [ @@ -39628,7 +39700,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ro": [ @@ -40632,7 +40706,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ru": [ @@ -41502,7 +41578,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "sk": [ @@ -42765,7 +42843,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "sl": [ @@ -44158,7 +44238,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "sr": [ @@ -45325,7 +45407,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "sv": [ @@ -46226,7 +46310,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "ta": [ @@ -47720,7 +47806,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "th": [ @@ -49168,7 +49256,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "tr": [ @@ -50032,7 +50122,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "uk": [ @@ -50933,7 +51025,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "vi": [ @@ -52282,7 +52376,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "zh": [ @@ -53146,7 +53242,9 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ], "zh_Hant": [ @@ -54291,6 +54389,8 @@ "spaceAnalytics", "changeAnalyticsLanguage", "suggestToSpace", - "suggestToSpaceDesc" + "suggestToSpaceDesc", + "practice", + "noLanguagesSet" ] } From 401d8522f8fda3184d823f58517867b19d2ae6d2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 25 Jun 2024 12:53:54 -0400 Subject: [PATCH 08/15] only show practice activity button if showMessageButtons is enabled --- lib/pages/chat/events/message.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 086b5fbbf..c5756438c 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -587,7 +587,8 @@ class Message extends StatelessWidget { ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ - MessageButtons(toolbarController: toolbarController), + if (pangeaMessageEvent?.showMessageButtons ?? false) + MessageButtons(toolbarController: toolbarController), MessageReactions(event, timeline), ], ), From 50af1e55085941290367235245b022af85dba97e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 25 Jun 2024 13:20:58 -0400 Subject: [PATCH 09/15] make practice card text bot style, change opacity of icon buttons based on if they're available / enabled --- assets/l10n/intl_en.arb | 3 +- lib/pangea/enum/message_mode_enum.dart | 26 +++ lib/pangea/widgets/chat/message_toolbar.dart | 8 +- .../practice_activity_card.dart | 6 +- needed-translations.txt | 150 ++++++++++++------ 5 files changed, 138 insertions(+), 55 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b83a6ff08..056c6a347 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4057,5 +4057,6 @@ "suggestToSpace": "Suggest this space", "suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces", "practice": "Practice", - "noLanguagesSet": "No languages set" + "noLanguagesSet": "No languages set", + "noActivitiesFound": "No practice activities found for this message" } \ No newline at end of file diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart index f25140a9c..58753e5b5 100644 --- a/lib/pangea/enum/message_mode_enum.dart +++ b/lib/pangea/enum/message_mode_enum.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -79,4 +80,29 @@ extension MessageModeExtension on MessageMode { return true; } } + + Color? iconColor( + PangeaMessageEvent event, + MessageMode? currentMode, + BuildContext context, + ) { + final bool isPracticeActivity = this == MessageMode.practiceActivity; + final bool practicing = currentMode == MessageMode.practiceActivity; + final bool practiceEnabled = event.hasUncompletedActivity; + + // if this is the practice activity icon, and there's no practice activities available, + // and the current mode is not practice, return lower opacity color. + if (isPracticeActivity && !practicing && !practiceEnabled) { + return Theme.of(context).iconTheme.color?.withOpacity(0.5); + } + + // if this is not a practice activity icon, and practice activities are available, + // then return lower opacity color if the current mode is practice. + if (!isPracticeActivity && practicing && practiceEnabled) { + return Theme.of(context).iconTheme.color?.withOpacity(0.5); + } + + // if this is the current mode, return primary color. + return currentMode == this ? Theme.of(context).colorScheme.primary : null; + } } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 9205a4246..1ce5f2f3d 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -418,9 +418,11 @@ class MessageToolbarState extends State { message: mode.tooltip(context), child: IconButton( icon: Icon(mode.icon), - color: currentMode == mode - ? Theme.of(context).colorScheme.primary - : null, + color: mode.iconColor( + widget.pangeaMessageEvent, + currentMode, + context, + ), onPressed: () => updateMode(mode), ), ); diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 18713c2c9..ae8028702 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -61,7 +62,10 @@ class MessagePracticeActivityCardState extends State { @override Widget build(BuildContext context) { if (practiceEvent == null) { - return const Text('No practice activities found for this message'); + return Text( + L10n.of(context)!.noActivitiesFound, + style: BotStyle.text(context), + ); // return GeneratePracticeActivityButton( // pangeaMessageEvent: widget.pangeaMessageEvent, // onActivityGenerated: updatePracticeActivity, diff --git a/needed-translations.txt b/needed-translations.txt index d7704d03e..1355bb368 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -862,7 +862,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "be": [ @@ -2361,7 +2362,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "bn": [ @@ -3856,7 +3858,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "bo": [ @@ -5355,7 +5358,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ca": [ @@ -6256,7 +6260,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "cs": [ @@ -7239,7 +7244,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "de": [ @@ -8105,7 +8111,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "el": [ @@ -9555,7 +9562,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "eo": [ @@ -10703,7 +10711,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "es": [ @@ -10717,7 +10726,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "et": [ @@ -11583,7 +11593,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "eu": [ @@ -12451,7 +12462,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "fa": [ @@ -13456,7 +13468,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "fi": [ @@ -14425,7 +14438,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "fil": [ @@ -15750,7 +15764,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "fr": [ @@ -16754,7 +16769,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ga": [ @@ -17887,7 +17903,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "gl": [ @@ -18753,7 +18770,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "he": [ @@ -20005,7 +20023,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "hi": [ @@ -21497,7 +21516,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "hr": [ @@ -22442,7 +22462,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "hu": [ @@ -23324,7 +23345,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ia": [ @@ -24809,7 +24831,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "id": [ @@ -25681,7 +25704,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ie": [ @@ -26937,7 +26961,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "it": [ @@ -27860,7 +27885,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ja": [ @@ -28894,7 +28920,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ka": [ @@ -30247,7 +30274,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ko": [ @@ -31115,7 +31143,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "lt": [ @@ -32149,7 +32178,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "lv": [ @@ -33023,7 +33053,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "nb": [ @@ -34221,7 +34252,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "nl": [ @@ -35183,7 +35215,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "pl": [ @@ -36154,7 +36187,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "pt": [ @@ -37631,7 +37665,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "pt_BR": [ @@ -38503,7 +38538,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "pt_PT": [ @@ -39702,7 +39738,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ro": [ @@ -40708,7 +40745,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ru": [ @@ -41580,7 +41618,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "sk": [ @@ -42845,7 +42884,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "sl": [ @@ -44240,7 +44280,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "sr": [ @@ -45409,7 +45450,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "sv": [ @@ -46312,7 +46354,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "ta": [ @@ -47808,7 +47851,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "th": [ @@ -49258,7 +49302,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "tr": [ @@ -50124,7 +50169,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "uk": [ @@ -51027,7 +51073,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "vi": [ @@ -52378,7 +52425,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "zh": [ @@ -53244,7 +53292,8 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ], "zh_Hant": [ @@ -54391,6 +54440,7 @@ "suggestToSpace", "suggestToSpaceDesc", "practice", - "noLanguagesSet" + "noLanguagesSet", + "noActivitiesFound" ] } From 965308d6289987d5d6e66026bc7c4f1c3eb1bd9c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 25 Jun 2024 14:12:33 -0400 Subject: [PATCH 10/15] show next activity after completing activity --- lib/pangea/widgets/chat/message_toolbar.dart | 6 ++- .../practice_activity_card.dart | 44 ++++++++++++++++--- .../practice_activity_content.dart | 5 ++- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 1ce5f2f3d..434128b1e 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -297,8 +297,10 @@ class MessageToolbarState extends State { } void showPracticeActivity() { - toolbarContent = - PracticeActivityCard(pangeaMessageEvent: widget.pangeaMessageEvent); + toolbarContent = PracticeActivityCard( + pangeaMessageEvent: widget.pangeaMessageEvent, + controller: this, + ); } void showImage() {} diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index ae8028702..c5ac1e6d1 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -1,8 +1,11 @@ import 'dart:developer'; +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -11,10 +14,12 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class PracticeActivityCard extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; + final MessageToolbarState controller; const PracticeActivityCard({ super.key, required this.pangeaMessageEvent, + required this.controller, }); @override @@ -31,7 +36,7 @@ class MessagePracticeActivityCardState extends State { loadInitialData(); } - void loadInitialData() { + String? get langCode { final String? langCode = MatrixState.pangeaController.languageController .activeL2Model() ?.langCode; @@ -41,24 +46,48 @@ class MessagePracticeActivityCardState extends State { SnackBar(content: Text(L10n.of(context)!.noLanguagesSet)), ); debugger(when: kDebugMode); - return; + return null; } + return langCode; + } - practiceEvent = - widget.pangeaMessageEvent.practiceActivities(langCode).firstOrNull; + void loadInitialData() { + if (langCode == null) return; + debugPrint( + "total events: ${widget.pangeaMessageEvent.practiceActivities(langCode!).length}", + ); + debugPrint( + "incomplete practice events: ${widget.pangeaMessageEvent.practiceActivities(langCode!).where((element) => !element.isComplete).length}", + ); + updatePracticeActivity(); + // practiceEvent = widget.pangeaMessageEvent + // .practiceActivities(langCode) + // .firstWhereOrNull((activity) => !activity.isComplete); if (practiceEvent == null) { debugger(when: kDebugMode); } - setState(() {}); } - void updatePracticeActivity(PracticeActivityEvent? newEvent) { + void updatePracticeActivity() { + if (langCode == null) return; setState(() { - practiceEvent = newEvent; + practiceEvent = widget.pangeaMessageEvent + .practiceActivities(langCode!) + .firstWhereOrNull( + (activity) => + activity.event.eventId != practiceEvent?.event.eventId && + !activity.isComplete, + ); }); } + void showNextActivity() { + if (langCode == null) return; + updatePracticeActivity(); + widget.controller.updateMode(MessageMode.practiceActivity); + } + @override Widget build(BuildContext context) { if (practiceEvent == null) { @@ -74,6 +103,7 @@ class MessagePracticeActivityCardState extends State { return PracticeActivityContent( practiceEvent: practiceEvent!, pangeaMessageEvent: widget.pangeaMessageEvent, + controller: this, ); } } diff --git a/lib/pangea/widgets/practice_activity/practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart index d5f387cfd..6f77741ed 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event. import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -14,11 +15,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class PracticeActivityContent extends StatefulWidget { final PracticeActivityEvent practiceEvent; final PangeaMessageEvent pangeaMessageEvent; + final MessagePracticeActivityCardState controller; const PracticeActivityContent({ super.key, required this.practiceEvent, required this.pangeaMessageEvent, + required this.controller, }); @override @@ -93,7 +96,7 @@ class MessagePracticeActivityContentState }, ); return null; - }); + }).then((_) => widget.controller.showNextActivity()); setState(() { recordSubmittedThisSession = true; From d959fdcc7bd6fa2c6eb1dc35f2ab5e0336fc2363 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 25 Jun 2024 14:24:13 -0400 Subject: [PATCH 11/15] Checks if visible rooms for my analytics --- lib/pages/chat_list/client_chooser_button.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index e45b38c33..4ad107f6b 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,5 +1,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart'; import 'package:fluffychat/pangea/utils/logout.dart'; import 'package:fluffychat/pangea/utils/space_code.dart'; @@ -68,7 +69,9 @@ class ClientChooserButton extends StatelessWidget { ), ), PopupMenuItem( - enabled: matrix.client.rooms.isNotEmpty, + enabled: matrix.client.rooms.any( + (room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom, + ), value: SettingsAction.myAnalytics, child: Row( children: [ From 2227c9d22d9f25c3de7aa5546649b013bc614f95 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 25 Jun 2024 14:41:45 -0400 Subject: [PATCH 12/15] update activity data when activity event id changes, show last activity if there are no incomplete activities --- .../practice_activity_card.dart | 36 +++++++++---------- .../practice_activity_content.dart | 14 ++++++++ 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index c5ac1e6d1..ce888f0fb 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -1,6 +1,5 @@ import 'dart:developer'; -import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; @@ -53,17 +52,7 @@ class MessagePracticeActivityCardState extends State { void loadInitialData() { if (langCode == null) return; - debugPrint( - "total events: ${widget.pangeaMessageEvent.practiceActivities(langCode!).length}", - ); - debugPrint( - "incomplete practice events: ${widget.pangeaMessageEvent.practiceActivities(langCode!).where((element) => !element.isComplete).length}", - ); updatePracticeActivity(); - // practiceEvent = widget.pangeaMessageEvent - // .practiceActivities(langCode) - // .firstWhereOrNull((activity) => !activity.isComplete); - if (practiceEvent == null) { debugger(when: kDebugMode); } @@ -71,15 +60,22 @@ class MessagePracticeActivityCardState extends State { void updatePracticeActivity() { if (langCode == null) return; - setState(() { - practiceEvent = widget.pangeaMessageEvent - .practiceActivities(langCode!) - .firstWhereOrNull( - (activity) => - activity.event.eventId != practiceEvent?.event.eventId && - !activity.isComplete, - ); - }); + final List activities = + widget.pangeaMessageEvent.practiceActivities(langCode!); + final List incompleteActivities = + activities.where((element) => !element.isComplete).toList(); + debugPrint("total events: ${activities.length}"); + debugPrint("incomplete practice events: ${incompleteActivities.length}"); + + // if an incomplete activity is found, show that + if (incompleteActivities.isNotEmpty) { + practiceEvent = incompleteActivities.first; + } + // if no incomplete activity is found, show the last activity + else if (activities.isNotEmpty) { + practiceEvent = activities.last; + } + setState(() {}); } void showNextActivity() { diff --git a/lib/pangea/widgets/practice_activity/practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart index 6f77741ed..563904e42 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -41,6 +41,19 @@ class MessagePracticeActivityContentState @override void initState() { super.initState(); + initalizeActivity(); + } + + @override + void didUpdateWidget(covariant PracticeActivityContent oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.practiceEvent.event.eventId != + widget.practiceEvent.event.eventId) { + initalizeActivity(); + } + } + + void initalizeActivity() { final PracticeActivityRecordEvent? recordEvent = widget.practiceEvent.userRecords.firstOrNull; if (recordEvent?.record == null) { @@ -54,6 +67,7 @@ class MessagePracticeActivityContentState recordSubmittedPreviousSession = true; recordSubmittedThisSession = true; } + setState(() {}); } void updateChoice(int index) { From 2cee0d374fb14726e46b1fc022896d0385070646 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 25 Jun 2024 14:51:47 -0400 Subject: [PATCH 13/15] Remove chat sorting edits --- lib/pages/chat_list/chat_list.dart | 33 ++++++------------------------ 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 9d7ff2440..569eba880 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; @@ -213,32 +212,12 @@ class ChatListController extends State } List get filteredRooms => Matrix.of(context) - .client - .rooms - .where( - getRoomFilterByActiveFilter(activeFilter), - ) - // #Pangea - .sorted((roomA, roomB) { - // put rooms with unread messages at the top of the list - if (roomA.membership == Membership.invite && - roomB.membership != Membership.invite) { - return -1; - } - if (roomA.membership != Membership.invite && - roomB.membership == Membership.invite) { - return 1; - } - - final bool aUnread = roomA.notificationCount > 0 || roomA.markedUnread; - final bool bUnread = roomB.notificationCount > 0 || roomB.markedUnread; - if (aUnread && !bUnread) return -1; - if (!aUnread && bUnread) return 1; - - return 0; - }) - // Pangea# - .toList(); + .client + .rooms + .where( + getRoomFilterByActiveFilter(activeFilter), + ) + .toList(); bool isSearchMode = false; Future? publicRoomsResponse; From a9ee9061b3ee178d1dabe9455129639be4b31c9d Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Tue, 25 Jun 2024 15:24:58 -0400 Subject: [PATCH 14/15] fixing latestResponse --- .../multiple_choice_activity_model.dart | 2 ++ .../practice_activity_record_model.dart | 4 ++-- .../widgets/practice_activity/practice_activity_card.dart | 6 ++++++ .../practice_activity/practice_activity_content.dart | 8 +++++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart index e152c18a2..3cd78a66a 100644 --- a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart @@ -18,6 +18,8 @@ class MultipleChoice { int get correctAnswerIndex => choices.indexOf(answer); + int choiceIndex(String choice) => choices.indexOf(choice); + Color choiceColor(int index) => index == correctAnswerIndex ? AppConfig.success : AppConfig.warning; diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart index 93ec8e478..3fe3e859d 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -40,12 +40,12 @@ class PracticeActivityRecordModel { /// get the latest response index according to the response timeStamp /// sort the responses by timestamp and get the index of the last response - int? get latestResponseIndex { + String? get latestResponse { if (responses.isEmpty) { return null; } responses.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - return responses.length - 1; + return responses[responses.length - 1].text; } void addResponse({ diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index ce888f0fb..f772bdfc8 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -50,6 +50,7 @@ class MessagePracticeActivityCardState extends State { return langCode; } +<<<<<<< Updated upstream void loadInitialData() { if (langCode == null) return; updatePracticeActivity(); @@ -75,6 +76,11 @@ class MessagePracticeActivityCardState extends State { else if (activities.isNotEmpty) { practiceEvent = activities.last; } +======= + practiceEvent = + widget.pangeaMessageEvent.practiceActivities(langCode).firstOrNull; + +>>>>>>> Stashed changes setState(() {}); } diff --git a/lib/pangea/widgets/practice_activity/practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart index 563904e42..8080c27ee 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -63,7 +63,13 @@ class MessagePracticeActivityContentState ); } else { recordModel = recordEvent!.record; - selectedChoiceIndex = recordModel!.latestResponseIndex; + + //Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget + selectedChoiceIndex = recordModel?.latestResponse != null + ? widget.practiceEvent.practiceActivity.multipleChoice + ?.choiceIndex(recordModel!.latestResponse!) + : null; + recordSubmittedPreviousSession = true; recordSubmittedThisSession = true; } From 22af3d096a02dc5d284076a5a72e217fc7eb3887 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Tue, 25 Jun 2024 15:33:30 -0400 Subject: [PATCH 15/15] merged and resolved conflicy --- .../widgets/practice_activity/practice_activity_card.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index f772bdfc8..ce888f0fb 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -50,7 +50,6 @@ class MessagePracticeActivityCardState extends State { return langCode; } -<<<<<<< Updated upstream void loadInitialData() { if (langCode == null) return; updatePracticeActivity(); @@ -76,11 +75,6 @@ class MessagePracticeActivityCardState extends State { else if (activities.isNotEmpty) { practiceEvent = activities.last; } -======= - practiceEvent = - widget.pangeaMessageEvent.practiceActivities(langCode).firstOrNull; - ->>>>>>> Stashed changes setState(() {}); }