From 425779e8687e422220ced3efe97403af9524af4b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 09:43:17 -0500 Subject: [PATCH 01/23] redact the current activity event when submitting feedback --- ...actice_activity_generation_controller.dart | 47 +++++++++++++++---- .../practice_activity_card.dart | 31 +++++++++--- pubspec.yaml | 2 +- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 410f8eeaa..c7238c9cc 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -19,7 +19,7 @@ import 'package:matrix/matrix.dart'; /// Represents an item in the completion cache. class _RequestCacheItem { MessageActivityRequest req; - PracticeActivityModel? practiceActivity; + PracticeActivityModelResponse? practiceActivity; _RequestCacheItem({ required this.req, @@ -99,7 +99,7 @@ class PracticeGenerationController { //TODO - allow return of activity content before sending the event // this requires some downstream changes to the way the event is handled - Future getPracticeActivity( + Future getPracticeActivity( MessageActivityRequest req, PangeaMessageEvent event, ) async { @@ -119,6 +119,8 @@ class PracticeGenerationController { return null; } + final eventCompleter = Completer(); + // if the server points to an existing event, return that event if (res.existingActivityEventId != null) { final Event? existingEvent = @@ -127,11 +129,19 @@ class PracticeGenerationController { debugPrint( 'Existing activity event found: ${existingEvent?.content}', ); - if (existingEvent != null) { - return PracticeActivityEvent( + debugPrint( + "eventID: ${existingEvent?.eventId}, event is redacted: ${existingEvent?.redacted}", + ); + if (existingEvent != null && !existingEvent.redacted) { + final activityEvent = PracticeActivityEvent( event: existingEvent, timeline: event.timeline, - ).practiceActivity; + ); + eventCompleter.complete(activityEvent); + return PracticeActivityModelResponse( + activity: activityEvent.practiceActivity, + eventCompleter: eventCompleter, + ); } } @@ -141,11 +151,30 @@ class PracticeGenerationController { } debugPrint('Activity generated: ${res.activity!.toJson()}'); + _sendAndPackageEvent(res.activity!, event).then((event) { + eventCompleter.complete(event); + }); - _sendAndPackageEvent(res.activity!, event); - _cache[cacheKey] = - _RequestCacheItem(req: req, practiceActivity: res.activity!); + final responseModel = PracticeActivityModelResponse( + activity: res.activity!, + eventCompleter: eventCompleter, + ); - return _cache[cacheKey]!.practiceActivity; + _cache[cacheKey] = _RequestCacheItem( + req: req, + practiceActivity: responseModel, + ); + + return responseModel; } } + +class PracticeActivityModelResponse { + final PracticeActivityModel? activity; + final Completer eventCompleter; + + PracticeActivityModelResponse({ + required this.activity, + required this.eventCompleter, + }); +} diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 1e97f2fe2..00b11f658 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.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_activity_event.dart'; @@ -45,6 +46,8 @@ class PracticeActivityCard extends StatefulWidget { class PracticeActivityCardState extends State { PracticeActivityModel? currentActivity; + Completer? currentActivityCompleter; + PracticeActivityRecordModel? currentCompletionRecord; bool fetchingActivity = false; @@ -133,9 +136,9 @@ class PracticeActivityCardState extends State { return null; } - final PracticeActivityModel? ourNewActivity = await pangeaController - .practiceGenerationController - .getPracticeActivity( + final PracticeActivityModelResponse? activityResponse = + await pangeaController.practiceGenerationController + .getPracticeActivity( MessageActivityRequest( userL1: pangeaController.languageController.userL1!.langCode, userL2: pangeaController.languageController.userL2!.langCode, @@ -157,9 +160,10 @@ class PracticeActivityCardState extends State { widget.pangeaMessageEvent, ); + currentActivityCompleter = activityResponse?.eventCompleter; _updateFetchingActivity(false); - return ourNewActivity; + return activityResponse?.activity; } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError( @@ -255,12 +259,27 @@ class PracticeActivityCardState extends State { /// clear the current activity, record, and selection /// fetch a new activity, including the offending activity in the request - void submitFeedback(String feedback) { - if (currentActivity == null) { + Future submitFeedback(String feedback) async { + if (currentActivity == null || currentCompletionRecord == null) { debugger(when: kDebugMode); return; } + if (currentActivityCompleter != null) { + final activityEvent = await currentActivityCompleter!.future; + await activityEvent?.event.redactEvent(reason: feedback); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception('No completer found for current activity'), + data: { + 'activity': currentActivity, + 'record': currentCompletionRecord, + 'feedback': feedback, + }, + ); + } + _fetchNewActivity( ActivityQualityFeedback( feedbackText: feedback, diff --git a/pubspec.yaml b/pubspec.yaml index 25c564cef..ca105194e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.23.3+3562 +version: 1.23.4+3563 environment: sdk: ">=3.0.0 <4.0.0" From c315d5b97078adf4c21e23dc94fcb79d58bfde77 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 11:31:44 -0500 Subject: [PATCH 02/23] added press animations to toolbar buttons --- lib/config/app_config.dart | 1 + lib/pangea/enum/message_mode_enum.dart | 6 +- .../chat/message_selection_overlay.dart | 4 +- .../widgets/chat/message_toolbar_buttons.dart | 75 ++++++++----- lib/pangea/widgets/pressable_button.dart | 106 ++++++++++++++++++ 5 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 lib/pangea/widgets/pressable_button.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index ab05ffd99..bec1df519 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -25,6 +25,7 @@ abstract class AppConfig { static const double toolbarMaxHeight = 300.0; static const double toolbarMinHeight = 70.0; static const double toolbarMinWidth = 270.0; + static const double toolbarButtonsHeight = 50.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); // static const Color primaryColorLight = Color(0xFFCCBDEA); diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart index cfc42f63b..f8ad41a5b 100644 --- a/lib/pangea/enum/message_mode_enum.dart +++ b/lib/pangea/enum/message_mode_enum.dart @@ -101,11 +101,7 @@ extension MessageModeExtension on MessageMode { } //unlocked and active - if (this == currentMode) { - return Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.primary; - } + if (this == currentMode) return Theme.of(context).colorScheme.primary; //unlocked and inactive return Theme.of(context).colorScheme.primaryContainer; diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 4c5cf86da..ea42c9854 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -281,9 +281,9 @@ class MessageOverlayController extends State return reactionsEvents.where((e) => !e.redacted).isNotEmpty; } - final double toolbarButtonsHeight = 50; double get reactionsHeight => hasReactions ? 28 : 0; - double get belowMessageHeight => toolbarButtonsHeight + reactionsHeight; + double get belowMessageHeight => + AppConfig.toolbarButtonsHeight + reactionsHeight; void setIsPlayingAudio(bool isPlaying) { if (mounted) { diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 7cead7b5a..75cd4c5c3 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/config/themes.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/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/pressable_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -31,6 +32,7 @@ class ToolbarButtons extends StatelessWidget { MatrixState.pangeaController.languageController.userL2?.langCode; static const double iconWidth = 36.0; + static const buttonSize = 40.0; @override Widget build(BuildContext context) { @@ -44,7 +46,7 @@ class ToolbarButtons extends StatelessWidget { return SizedBox( width: width, - height: 50, + height: AppConfig.toolbarButtonsHeight, child: Stack( alignment: Alignment.center, children: [ @@ -75,37 +77,50 @@ class ToolbarButtons extends StatelessWidget { ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: modes - .mapIndexed( - (index, mode) => IconButton( - iconSize: 20, - icon: Icon(mode.icon), - tooltip: mode.tooltip(context), - color: mode == overlayController.toolbarMode - ? Colors.white - : null, - isSelected: mode == overlayController.toolbarMode, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - mode.iconButtonColor( - context, - index, - overlayController.toolbarMode, - pangeaMessageEvent.numberOfActivitiesCompleted, - totallyDone, - ), - ), + children: modes.mapIndexed((index, mode) { + final enabled = mode.isUnlocked( + index, + pangeaMessageEvent.numberOfActivitiesCompleted, + totallyDone, + ); + final color = mode.iconButtonColor( + context, + index, + overlayController.toolbarMode, + pangeaMessageEvent.numberOfActivitiesCompleted, + totallyDone, + ); + return Tooltip( + message: mode.tooltip(context), + child: PressableButton( + width: buttonSize, + height: buttonSize, + borderRadius: BorderRadius.circular(20), + enabled: enabled, + depressed: !enabled || mode == overlayController.toolbarMode, + color: color, + onPressed: enabled + ? () => overlayController.updateToolbarMode(mode) + : null, + child: AnimatedContainer( + duration: FluffyThemes.animationDuration, + height: buttonSize, + width: buttonSize, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Icon( + mode.icon, + size: 20, + color: mode == overlayController.toolbarMode + ? Colors.white + : null, ), - onPressed: mode.isUnlocked( - index, - pangeaMessageEvent.numberOfActivitiesCompleted, - totallyDone, - ) - ? () => overlayController.updateToolbarMode(mode) - : null, ), - ) - .toList(), + ), + ); + }).toList(), ), ], ), diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart new file mode 100644 index 000000000..b112dff0d --- /dev/null +++ b/lib/pangea/widgets/pressable_button.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PressableButton extends StatefulWidget { + final double width; + final double height; + final BorderRadius borderRadius; + + final bool enabled; + final bool depressed; + + final Color color; + final Widget child; + final void Function()? onPressed; + + const PressableButton({ + required this.width, + required this.height, + required this.borderRadius, + required this.child, + required this.onPressed, + required this.color, + this.enabled = true, + this.depressed = false, + super.key, + }); + + @override + PressableButtonState createState() => PressableButtonState(); +} + +class PressableButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _tweenAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + _tweenAnimation = Tween(begin: 5, end: 0).animate(_controller); + } + + void _onTapDown(TapDownDetails details) { + if (!widget.enabled) return; + _controller.forward(); + } + + void _onTapUp(TapUpDetails details) { + if (!widget.enabled) return; + _controller.reverse(); + widget.onPressed?.call(); + HapticFeedback.mediumImpact(); + } + + void _onTapCancel() { + if (!widget.enabled) return; + _controller.reverse(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: SizedBox( + height: 45, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.black.withOpacity(0.25), + widget.color, + ), + borderRadius: widget.borderRadius, + ), + ), + AnimatedBuilder( + animation: _tweenAnimation, + builder: (context, _) { + return Positioned( + bottom: widget.depressed ? 0 : _tweenAnimation.value, + child: widget.child, + ); + }, + ), + ], + ), + ), + ); + } +} From 7beb14dfafe1b65d0178e3669cb30bb48e0b1490 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 11:55:05 -0500 Subject: [PATCH 03/23] reorder options in chat header --- .../chat/message_selection_overlay.dart | 64 +++++++++---------- lib/pangea/widgets/chat/overlay_header.dart | 48 ++++++-------- 2 files changed, 50 insertions(+), 62 deletions(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index ea42c9854..f614bb4e2 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -550,7 +550,13 @@ class MessageOverlayController extends State messageOffset!.dy - messageSize!.height - belowMessageHeight, - child: overlayMessage, + child: Padding( + padding: EdgeInsets.only( + left: horizontalPadding, + right: horizontalPadding, + ), + child: overlayMessage, + ), ) : AnimatedBuilder( animation: _overlayPositionAnimation!, @@ -564,39 +570,33 @@ class MessageOverlayController extends State }, ); - return Padding( - padding: EdgeInsets.only( - left: horizontalPadding, - right: horizontalPadding, - ), - child: Stack( - children: [ - positionedOverlayMessage, - Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - OverlayFooter(controller: widget.chatController), - ], - ), + return Stack( + children: [ + positionedOverlayMessage, + Align( + alignment: Alignment.bottomCenter, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + OverlayFooter(controller: widget.chatController), + ], ), - if (showDetails) - const SizedBox( - width: FluffyThemes.columnWidth, - ), - ], - ), + ), + if (showDetails) + const SizedBox( + width: FluffyThemes.columnWidth, + ), + ], ), - Material( - child: OverlayHeader(controller: widget.chatController), - ), - ], - ), + ), + Material( + child: OverlayHeader(controller: widget.chatController), + ), + ], ); } } diff --git a/lib/pangea/widgets/chat/overlay_header.dart b/lib/pangea/widgets/chat/overlay_header.dart index cce47adcc..7588cea8a 100644 --- a/lib/pangea/widgets/chat/overlay_header.dart +++ b/lib/pangea/widgets/chat/overlay_header.dart @@ -1,7 +1,7 @@ import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix.dart'; class OverlayHeader extends StatelessWidget { @@ -21,21 +21,12 @@ class OverlayHeader extends StatelessWidget { actionsIconTheme: IconThemeData( color: Theme.of(context).colorScheme.primary, ), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context)!.close, - color: Theme.of(context).colorScheme.primary, - ), - titleSpacing: 0, - title: ChatAppBarTitle(controller), actions: [ - if (controller.canEditSelectedEvents) - IconButton( - icon: const Icon(Icons.edit_outlined), - tooltip: L10n.of(context)!.edit, - onPressed: controller.editSelectedEventAction, - ), + IconButton( + icon: const Icon(Symbols.forward), + tooltip: L10n.of(context)!.forward, + onPressed: controller.forwardEventsAction, + ), if (controller.selectedEvents.length == 1 && controller.selectedEvents.single.messageType == MessageTypes.Text) @@ -44,27 +35,30 @@ class OverlayHeader extends StatelessWidget { tooltip: L10n.of(context)!.copy, onPressed: controller.copyEventsAction, ), - if (controller.canSaveSelectedEvent) - // Use builder context to correctly position the share dialog on iPad - Builder( - builder: (context) => IconButton( - icon: Icon(Icons.adaptive.share), - tooltip: L10n.of(context)!.share, - onPressed: () => controller.saveSelectedEvent(context), - ), - ), if (controller.canPinSelectedEvents) IconButton( icon: const Icon(Icons.push_pin_outlined), onPressed: controller.pinEvent, tooltip: L10n.of(context)!.pinMessage, ), + if (controller.canEditSelectedEvents) + IconButton( + icon: const Icon(Icons.edit_outlined), + tooltip: L10n.of(context)!.edit, + onPressed: controller.editSelectedEventAction, + ), if (controller.canRedactSelectedEvents) IconButton( icon: const Icon(Icons.delete_outlined), tooltip: L10n.of(context)!.redactMessage, onPressed: controller.redactEventsAction, ), + if (controller.selectedEvents.length == 1) + IconButton( + icon: const Icon(Icons.shield_outlined), + tooltip: L10n.of(context)!.reportMessage, + onPressed: controller.reportEventAction, + ), if (controller.selectedEvents.length == 1) IconButton( icon: const Icon(Icons.info_outlined), @@ -74,12 +68,6 @@ class OverlayHeader extends StatelessWidget { controller.clearSelectedEvents(); }, ), - if (controller.selectedEvents.length == 1) - IconButton( - icon: const Icon(Icons.shield_outlined), - tooltip: L10n.of(context)!.reportMessage, - onPressed: controller.reportEventAction, - ), ], ), ], From 8330f2c505f30e931a13b7b6378e27d0cf522d24 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 12:58:29 -0500 Subject: [PATCH 04/23] mmove reaction picker down into row with reply button --- lib/pages/chat/chat_input_row.dart | 97 +++++++++++-------- lib/pangea/widgets/chat/overlay_footer.dart | 10 +- .../widgets/chat/pangea_reaction_picker.dart | 86 ++++++++++++++++ 3 files changed, 143 insertions(+), 50 deletions(-) create mode 100644 lib/pangea/widgets/chat/pangea_reaction_picker.dart diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 340784455..90b9c0793 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,11 +1,13 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; +import 'package:fluffychat/pangea/widgets/chat/pangea_reaction_picker.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix.dart'; import '../../config/themes.dart'; @@ -55,9 +57,9 @@ class ChatInputRow extends StatelessWidget { children: [ Row( // crossAxisAlignment: CrossAxisAlignment.end, + // mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, // Pangea# - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: controller.selectMode ? [ if (controller.selectedEvents @@ -77,50 +79,61 @@ class ChatInputRow extends StatelessWidget { ), ), ) + // #Pangea + // else + // SizedBox( + // height: height, + // child: TextButton( + // onPressed: controller.forwardEventsAction, + // child: Row( + // children: [ + // const Icon(Icons.keyboard_arrow_left_outlined), + // Text(L10n.of(context)!.forward), + // ], + // ), + // ), + // ), else - SizedBox( - height: height, - child: TextButton( - onPressed: controller.forwardEventsAction, - child: Row( - children: [ - const Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context)!.forward), - ], - ), - ), - ), - controller.selectedEvents.length == 1 - ? controller.selectedEvents.first - .getDisplayEvent(controller.timeline!) - .status - .isSent - ? SizedBox( - height: height, - child: TextButton( - onPressed: controller.replyAction, - child: Row( - children: [ - Text(L10n.of(context)!.reply), - const Icon(Icons.keyboard_arrow_right), - ], + // Pangea# + controller.selectedEvents.length == 1 + ? controller.selectedEvents.first + .getDisplayEvent(controller.timeline!) + .status + .isSent + ? SizedBox( + height: height, + child: TextButton( + onPressed: controller.replyAction, + child: Row( + children: [ + // #Pangea + // Text(L10n.of(context)!.reply), + // const Icon(Icons.keyboard_arrow_right), + const Icon(Symbols.reply), + const SizedBox(width: 6), + Text(L10n.of(context)!.reply), + // Pangea# + ], + ), ), - ), - ) - : SizedBox( - height: height, - child: TextButton( - onPressed: controller.sendAgainAction, - child: Row( - children: [ - Text(L10n.of(context)!.tryToSendAgain), - const SizedBox(width: 4), - const Icon(Icons.send_outlined, size: 16), - ], + ) + : SizedBox( + height: height, + child: TextButton( + onPressed: controller.sendAgainAction, + child: Row( + children: [ + Text(L10n.of(context)!.tryToSendAgain), + const SizedBox(width: 4), + const Icon(Icons.send_outlined, size: 16), + ], + ), ), - ), - ) - : const SizedBox.shrink(), + ) + : const SizedBox.shrink(), + // #Pangea + PangeaReactionsPicker(controller), + // Pangea# ] : [ // #Pangea diff --git a/lib/pangea/widgets/chat/overlay_footer.dart b/lib/pangea/widgets/chat/overlay_footer.dart index b4c51d07c..43c6acbb2 100644 --- a/lib/pangea/widgets/chat/overlay_footer.dart +++ b/lib/pangea/widgets/chat/overlay_footer.dart @@ -1,7 +1,6 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_input_row.dart'; -import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:flutter/material.dart'; class OverlayFooter extends StatelessWidget { @@ -18,7 +17,7 @@ class OverlayFooter extends StatelessWidget { return Container( margin: EdgeInsets.only( - bottom: bottomSheetPadding, + bottom: bottomSheetPadding + 16, left: bottomSheetPadding, right: bottomSheetPadding, ), @@ -34,12 +33,7 @@ class OverlayFooter extends StatelessWidget { borderRadius: const BorderRadius.all( Radius.circular(24), ), - child: Column( - children: [ - ReactionsPicker(controller), - ChatInputRow(controller), - ], - ), + child: ChatInputRow(controller), ), ], ), diff --git a/lib/pangea/widgets/chat/pangea_reaction_picker.dart b/lib/pangea/widgets/chat/pangea_reaction_picker.dart new file mode 100644 index 000000000..33f1a490f --- /dev/null +++ b/lib/pangea/widgets/chat/pangea_reaction_picker.dart @@ -0,0 +1,86 @@ +import 'package:fluffychat/config/app_emojis.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class PangeaReactionsPicker extends StatelessWidget { + final ChatController controller; + + const PangeaReactionsPicker(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (controller.showEmojiPicker) return const SizedBox.shrink(); + final display = controller.editEvent == null && + controller.replyEvent == null && + controller.room.canSendDefaultMessages && + controller.selectedEvents.isNotEmpty; + + if (!display) { + return const SizedBox.shrink(); + } + final emojis = List.from(AppEmojis.emojis); + final allReactionEvents = controller.selectedEvents.first + .aggregatedEvents( + controller.timeline!, + RelationshipTypes.reaction, + ) + .where( + (event) => + event.senderId == event.room.client.userID && + event.type == 'm.reaction', + ); + + for (final event in allReactionEvents) { + try { + emojis.remove(event.content.tryGetMap('m.relates_to')!['key']); + } catch (_) {} + } + return Flexible( + child: Row( + children: [ + Flexible( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: emojis + .map( + (emoji) => InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => controller.sendEmojiAction(emoji), + child: Container( + width: kIsWeb ? 56 : 48, + alignment: Alignment.center, + child: Text( + emoji, + style: const TextStyle(fontSize: 24), + ), + ), + ), + ) + .toList(), + ), + ), + ), + InkWell( + borderRadius: BorderRadius.circular(8), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + width: 36, + height: 56, + decoration: BoxDecoration( + color: theme.colorScheme.onInverseSurface, + shape: BoxShape.circle, + ), + child: const Icon(Icons.add_outlined), + ), + onTap: () => controller.pickEmojiReactionAction(allReactionEvents), + ), + ], + ), + ); + } +} From 7d80d8f6e2bc6e015ad82fc8ad4bf5c589f5de36 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 13:03:25 -0500 Subject: [PATCH 05/23] added back record button --- lib/pages/chat/chat_input_row.dart | 35 ++++++++++++++++-------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 90b9c0793..0167d5d21 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -349,22 +349,25 @@ class ChatInputRow extends StatelessWidget { height: height, width: height, alignment: Alignment.center, - child: + child: PlatformInfos.platformCanRecord && + controller.sendController.text.isEmpty + // #Pangea + && + !controller.choreographer.itController.willOpen + // Pangea# + ? FloatingActionButton.small( + tooltip: L10n.of(context)!.voiceMessage, + onPressed: controller.voiceMessageAction, + elevation: 0, + heroTag: null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(height), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + child: const Icon(Icons.mic_none_outlined), + ) // #Pangea - // PlatformInfos.platformCanRecord && - // controller.sendController.text.isEmpty - // ? FloatingActionButton.small( - // tooltip: L10n.of(context)!.voiceMessage, - // onPressed: controller.voiceMessageAction, - // elevation: 0, - // heroTag: null, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(height), - // ), - // backgroundColor: theme.colorScheme.primary, - // foregroundColor: theme.colorScheme.onPrimary, - // child: const Icon(Icons.mic_none_outlined), - // ) // : FloatingActionButton.small( // tooltip: L10n.of(context)!.send, // onPressed: controller.send, @@ -378,7 +381,7 @@ class ChatInputRow extends StatelessWidget { // foregroundColor: theme.colorScheme.onPrimary, // child: const Icon(Icons.send_outlined), // ), - ChoreographerSendButton(controller: controller), + : ChoreographerSendButton(controller: controller), // Pangea# ), ], From 43b663d66bf6b753bb4dc8e1422335f5428e5e08 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 13:19:03 -0500 Subject: [PATCH 06/23] revent change to padding in message overlay --- .../chat/message_selection_overlay.dart | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index f614bb4e2..ea42c9854 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -550,13 +550,7 @@ class MessageOverlayController extends State messageOffset!.dy - messageSize!.height - belowMessageHeight, - child: Padding( - padding: EdgeInsets.only( - left: horizontalPadding, - right: horizontalPadding, - ), - child: overlayMessage, - ), + child: overlayMessage, ) : AnimatedBuilder( animation: _overlayPositionAnimation!, @@ -570,33 +564,39 @@ class MessageOverlayController extends State }, ); - return Stack( - children: [ - positionedOverlayMessage, - Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - OverlayFooter(controller: widget.chatController), - ], + return Padding( + padding: EdgeInsets.only( + left: horizontalPadding, + right: horizontalPadding, + ), + child: Stack( + children: [ + positionedOverlayMessage, + Align( + alignment: Alignment.bottomCenter, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + OverlayFooter(controller: widget.chatController), + ], + ), ), - ), - if (showDetails) - const SizedBox( - width: FluffyThemes.columnWidth, - ), - ], + if (showDetails) + const SizedBox( + width: FluffyThemes.columnWidth, + ), + ], + ), ), - ), - Material( - child: OverlayHeader(controller: widget.chatController), - ), - ], + Material( + child: OverlayHeader(controller: widget.chatController), + ), + ], + ), ); } } From 4d805d82066f55b932c976e5f57b43a4aea9773c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 14:24:28 -0500 Subject: [PATCH 07/23] fixes audio message overlay warping --- .../chat/message_selection_overlay.dart | 7 +++++-- .../widgets/chat/message_toolbar_buttons.dart | 2 +- lib/pangea/widgets/chat/overlay_message.dart | 21 +++++++++++++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index ea42c9854..baa84ccc7 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -73,6 +73,8 @@ class MessageOverlayController extends State final TtsController tts = TtsController(); bool isPlayingAudio = false; + bool get showToolbarButtons => !widget._pangeaMessageEvent.isAudioMessage; + @override void initState() { super.initState(); @@ -281,9 +283,10 @@ class MessageOverlayController extends State return reactionsEvents.where((e) => !e.redacted).isNotEmpty; } + double get toolbarButtonsHeight => + showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0; double get reactionsHeight => hasReactions ? 28 : 0; - double get belowMessageHeight => - AppConfig.toolbarButtonsHeight + reactionsHeight; + double get belowMessageHeight => toolbarButtonsHeight + reactionsHeight; void setIsPlayingAudio(bool isPlaying) { if (mounted) { diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 75cd4c5c3..3976c7505 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -40,7 +40,7 @@ class ToolbarButtons extends StatelessWidget { overlayController.isPracticeComplete || !messageInUserL2; final double barWidth = width - iconWidth; - if (overlayController.pangeaMessageEvent.isAudioMessage) { + if (!overlayController.showToolbarButtons) { return const SizedBox(); } diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index 23c5cf267..c0a362fc3 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -82,6 +82,17 @@ class OverlayMessage extends StatelessWidget { : theme.colorScheme.primary; } + final noBubble = { + MessageTypes.Video, + MessageTypes.Image, + MessageTypes.Sticker, + }.contains(pangeaMessageEvent.event.messageType) && + !pangeaMessageEvent.event.redacted; + final noPadding = { + MessageTypes.File, + MessageTypes.Audio, + }.contains(pangeaMessageEvent.event.messageType); + return Material( color: color, clipBehavior: Clip.antiAlias, @@ -95,10 +106,12 @@ class OverlayMessage extends StatelessWidget { AppConfig.borderRadius, ), ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), width: messageWidth, height: messageHeight, child: MessageContent( From 8df77c6ed762de2586551d516b7c810d1ef466ce Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 5 Nov 2024 16:10:43 -0500 Subject: [PATCH 08/23] in message toolbar, use the display representation instead of originalSent so that immersion mode still works --- .../pangea_message_event.dart | 8 ++-- .../widgets/chat/message_audio_card.dart | 19 ++-------- .../chat/message_selection_overlay.dart | 8 +++- lib/pangea/widgets/chat/message_toolbar.dart | 37 ++++++++++++++++--- .../widgets/chat/overlay_message_text.dart | 26 +++++++------ lib/pangea/widgets/igc/pangea_rich_text.dart | 7 +--- .../practice_activity_card.dart | 25 ++++++------- .../target_tokens_controller.dart | 11 +++--- 8 files changed, 77 insertions(+), 64 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 74d4483dc..0a21003f5 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -564,13 +564,13 @@ class PangeaMessageEvent { return langCode ?? LanguageKeys.unknownLanguage; } + RepresentationEvent? get messageDisplayRepresentation => + representationByLanguage(messageDisplayLangCode); + /// Gets the message display text for the current language code. /// If the message display text is not available for the current language code, /// it returns the message body. - String get messageDisplayText { - final String? text = representationByLanguage(messageDisplayLangCode)?.text; - return text ?? body; - } + String get messageDisplayText => messageDisplayRepresentation?.text ?? body; List? errorSteps(String lemma) { final RepresentationEvent? repEvent = originalSent ?? originalWritten; diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index cc41605c9..5b6f9b539 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -14,7 +14,6 @@ import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class MessageAudioCard extends StatefulWidget { @@ -147,15 +146,10 @@ class MessageAudioCardState extends State { try { final String langCode = widget.messageEvent.messageDisplayLangCode; - final String? text = - widget.messageEvent.representationByLanguage(langCode)?.text; - - if (text == null) { - //TODO - handle error but get out of flow - } - - final Event? localEvent = - widget.messageEvent.getTextToSpeechLocal(langCode, text!); + final Event? localEvent = widget.messageEvent.getTextToSpeechLocal( + langCode, + widget.messageEvent.messageDisplayText, + ); if (localEvent != null) { audioFile = await localEvent.getPangeaAudioFile(); @@ -172,11 +166,6 @@ class MessageAudioCardState extends State { debugPrint(StackTrace.current.toString()); if (!mounted) return; setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.errorGettingAudio), - ), - ); ErrorHandler.logError( e: e, s: s, diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index baa84ccc7..541eee747 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -74,6 +75,8 @@ class MessageOverlayController extends State bool isPlayingAudio = false; bool get showToolbarButtons => !widget._pangeaMessageEvent.isAudioMessage; + final TargetTokensController targetTokensController = + TargetTokensController(); @override void initState() { @@ -106,8 +109,8 @@ class MessageOverlayController extends State }, ).listen((_) => setState(() {})); - setInitialToolbarMode(); tts.setupTTS(); + setInitialToolbarMode(); } /// We need to check if the setState call is safe to call immediately @@ -487,7 +490,8 @@ class MessageOverlayController extends State MessageToolbar( pangeaMessageEvent: widget._pangeaMessageEvent, overLayController: this, - tts: tts, + ttsController: tts, + targetTokensController: targetTokensController, ), SizedBox( height: adjustedMessageHeight, diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index ffca4f32c..0c5c80dfa 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -10,10 +10,13 @@ import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; 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/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; +import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/widgets/message_display_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -24,13 +27,15 @@ const double minCardHeight = 70; class MessageToolbar extends StatelessWidget { final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overLayController; - final TtsController tts; + final TtsController ttsController; + final TargetTokensController targetTokensController; const MessageToolbar({ super.key, required this.pangeaMessageEvent, required this.overLayController, - required this.tts, + required this.ttsController, + required this.targetTokensController, }); Widget toolbarContent(BuildContext context) { @@ -58,7 +63,7 @@ class MessageToolbar extends StatelessWidget { messageEvent: pangeaMessageEvent, overlayController: overLayController, selection: overLayController.selectedSpan, - tts: tts, + tts: ttsController, setIsPlayingAudio: overLayController.setIsPlayingAudio, ); case MessageMode.speechToText: @@ -67,8 +72,27 @@ class MessageToolbar extends StatelessWidget { ); case MessageMode.definition: if (!overLayController.isSelection) { - return MessageDisplayCard( - displayText: L10n.of(context)!.selectToDefine, + return FutureBuilder( + future: targetTokensController.targetTokens(pangeaMessageEvent), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const ToolbarContentLoadingIndicator(); + } else if (snapshot.hasError || + snapshot.data == null || + snapshot.data!.isEmpty) { + return const Padding( + padding: EdgeInsets.all(8), + child: CardErrorWidget( + error: "No tokens available", + maxWidth: AppConfig.toolbarMinWidth, + ), + ); + } else { + return MessageDisplayCard( + displayText: L10n.of(context)!.selectToDefine, + ); + } + }, ); } else { try { @@ -106,7 +130,8 @@ class MessageToolbar extends StatelessWidget { return PracticeActivityCard( pangeaMessageEvent: pangeaMessageEvent, overlayController: overLayController, - tts: tts, + ttsController: ttsController, + targetTokensController: targetTokensController, ); default: debugger(when: kDebugMode); diff --git a/lib/pangea/widgets/chat/overlay_message_text.dart b/lib/pangea/widgets/chat/overlay_message_text.dart index f23cbba5c..79f5b8a91 100644 --- a/lib/pangea/widgets/chat/overlay_message_text.dart +++ b/lib/pangea/widgets/chat/overlay_message_text.dart @@ -29,17 +29,19 @@ class OverlayMessageTextState extends State { @override void initState() { - tokens = widget.pangeaMessageEvent.originalSent?.tokens; - if (widget.pangeaMessageEvent.originalSent != null && tokens == null) { - widget.pangeaMessageEvent.originalSent! - .tokensGlobal( - widget.pangeaMessageEvent.senderId, - widget.pangeaMessageEvent.originServerTs, - ) - .then((tokens) { - // this isn't currently working because originalSent's _event is null - setState(() => this.tokens = tokens); - }); + final repEvent = widget.pangeaMessageEvent.messageDisplayRepresentation; + if (repEvent != null) { + tokens = repEvent.tokens; + if (tokens == null) { + repEvent + .tokensGlobal( + widget.pangeaMessageEvent.senderId, + widget.pangeaMessageEvent.originServerTs, + ) + .then((tokens) { + setState(() => this.tokens = tokens); + }); + } } super.initState(); } @@ -70,7 +72,7 @@ class OverlayMessageTextState extends State { // Convert the entire message into a list of characters final Characters messageCharacters = - widget.pangeaMessageEvent.event.body.characters; + widget.pangeaMessageEvent.messageDisplayText.characters; // When building token positions, use grapheme cluster indices final List tokenPositions = []; diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 9da9b45b6..8eecdd2f8 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -91,12 +91,7 @@ class PangeaRichTextState extends State { debugger(when: kDebugMode); } - repEvent = widget.pangeaMessageEvent - .representationByLanguage( - widget.pangeaMessageEvent.messageDisplayLangCode, - ) - ?.content; - + repEvent = widget.pangeaMessageEvent.messageDisplayRepresentation?.content; if (repEvent == null) { setState(() => _fetchingRepresentation = true); widget.pangeaMessageEvent diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 00b11f658..6692bd1ce 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -31,13 +31,15 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class PracticeActivityCard extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overlayController; - final TtsController tts; + final TtsController ttsController; + final TargetTokensController targetTokensController; const PracticeActivityCard({ super.key, required this.pangeaMessageEvent, required this.overlayController, - required this.tts, + required this.ttsController, + required this.targetTokensController, }); @override @@ -51,10 +53,6 @@ class PracticeActivityCardState extends State { PracticeActivityRecordModel? currentCompletionRecord; bool fetchingActivity = false; - // tracks the target tokens for the current message - // in a separate controller to manage the state - TargetTokensController targetTokensController = TargetTokensController(); - List get practiceActivities => widget.pangeaMessageEvent.practiceActivities; @@ -124,7 +122,7 @@ class PracticeActivityCardState extends State { return null; } - if (widget.pangeaMessageEvent.originalSent == null) { + if (widget.pangeaMessageEvent.messageDisplayRepresentation == null) { debugger(when: kDebugMode); _updateFetchingActivity(false); ErrorHandler.logError( @@ -142,8 +140,8 @@ class PracticeActivityCardState extends State { MessageActivityRequest( userL1: pangeaController.languageController.userL1!.langCode, userL2: pangeaController.languageController.userL2!.langCode, - messageText: widget.pangeaMessageEvent.originalSent!.text, - tokensWithXP: await targetTokensController.targetTokens( + messageText: widget.pangeaMessageEvent.messageDisplayText, + tokensWithXP: await widget.targetTokensController.targetTokens( widget.pangeaMessageEvent, ), messageId: widget.pangeaMessageEvent.eventId, @@ -151,7 +149,8 @@ class PracticeActivityCardState extends State { .map((activity) => activity.activityRequestMetaData) .toList(), activityQualityFeedback: activityFeedback, - clientCompatibleActivities: widget.tts.isLanguageFullySupported + clientCompatibleActivities: widget + .ttsController.isLanguageFullySupported ? ActivityTypeEnum.values : ActivityTypeEnum.values .where((type) => type != ActivityTypeEnum.wordFocusListening) @@ -221,7 +220,7 @@ class PracticeActivityCardState extends State { // update the target tokens with the new construct uses // NOTE - multiple choice activity is handling adding these to analytics - await targetTokensController.updateTokensWithConstructs( + await widget.targetTokensController.updateTokensWithConstructs( currentCompletionRecord!.usesForAllResponses( currentActivity!, metadata, @@ -320,7 +319,7 @@ class PracticeActivityCardState extends State { return MultipleChoiceActivity( practiceCardController: this, currentActivity: currentActivity!, - tts: widget.tts, + tts: widget.ttsController, eventID: widget.pangeaMessageEvent.eventId, ); case ActivityTypeEnum.wordFocusListening: @@ -329,7 +328,7 @@ class PracticeActivityCardState extends State { return MultipleChoiceActivity( practiceCardController: this, currentActivity: currentActivity!, - tts: widget.tts, + tts: widget.ttsController, eventID: widget.pangeaMessageEvent.eventId, ); // default: diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart index 69be7f6c2..f424d0d50 100644 --- a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -38,12 +38,11 @@ class TargetTokensController { Future> _initialize( PangeaMessageEvent pangeaMessageEvent, ) async { - final tokens = await pangeaMessageEvent - .representationByLanguage(pangeaMessageEvent.messageDisplayLangCode) - ?.tokensGlobal( - pangeaMessageEvent.senderId, - pangeaMessageEvent.originServerTs, - ); + final tokens = + await pangeaMessageEvent.messageDisplayRepresentation?.tokensGlobal( + pangeaMessageEvent.senderId, + pangeaMessageEvent.originServerTs, + ); if (tokens == null || tokens.isEmpty) { debugger(when: kDebugMode); From e48d8d57c9d7c33997dd74a8fa33b5f428790bc0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 09:30:11 -0500 Subject: [PATCH 09/23] filter redacted events from _practiceActivityEvents to prevent them from being sent to the choreographer --- lib/pangea/matrix_event_wrappers/pangea_message_event.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 74d4483dc..a06d4df7e 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -596,6 +596,7 @@ class PangeaMessageEvent { timeline, PangeaEventTypes.pangeaActivity, ) + .where((event) => !event.redacted) .toList(); final List practiceEvents = []; From d5f8be57b5d8bee6215adabc7eb7973018807e94 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 11:58:08 -0500 Subject: [PATCH 10/23] added buttonHeight as parameter of pressable button and added completer to prevent up animation from starting before the down animation finishes --- lib/pangea/widgets/pressable_button.dart | 25 ++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart index b112dff0d..40d9be423 100644 --- a/lib/pangea/widgets/pressable_button.dart +++ b/lib/pangea/widgets/pressable_button.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -5,6 +7,7 @@ class PressableButton extends StatefulWidget { final double width; final double height; final BorderRadius borderRadius; + final double buttonHeight; final bool enabled; final bool depressed; @@ -20,6 +23,7 @@ class PressableButton extends StatefulWidget { required this.child, required this.onPressed, required this.color, + this.buttonHeight = 5, this.enabled = true, this.depressed = false, super.key, @@ -33,6 +37,7 @@ class PressableButtonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _tweenAnimation; + Completer? _animationCompleter; @override void initState() { @@ -41,16 +46,24 @@ class PressableButtonState extends State duration: const Duration(milliseconds: 100), vsync: this, ); - _tweenAnimation = Tween(begin: 5, end: 0).animate(_controller); + _tweenAnimation = + Tween(begin: widget.buttonHeight, end: 0).animate(_controller); } void _onTapDown(TapDownDetails details) { if (!widget.enabled) return; - _controller.forward(); + _animationCompleter = Completer(); + _controller.forward().then((_) { + _animationCompleter?.complete(); + _animationCompleter = null; + }); } - void _onTapUp(TapUpDetails details) { + Future _onTapUp(TapUpDetails details) async { if (!widget.enabled) return; + if (_animationCompleter != null) { + await _animationCompleter!.future; + } _controller.reverse(); widget.onPressed?.call(); HapticFeedback.mediumImpact(); @@ -73,14 +86,14 @@ class PressableButtonState extends State onTapDown: _onTapDown, onTapUp: _onTapUp, onTapCancel: _onTapCancel, - child: SizedBox( - height: 45, + child: Container( + decoration: BoxDecoration(border: Border.all(color: Colors.green)), child: Stack( alignment: Alignment.bottomCenter, children: [ Container( width: widget.width, - height: widget.height, + // height: widget.height, decoration: BoxDecoration( color: Color.alphaBlend( Colors.black.withOpacity(0.25), From a2513c7bd48d04c3db0c78c2c6101718040ef264 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 11:59:19 -0500 Subject: [PATCH 11/23] stretch toolbar button rows to hold buttons --- lib/pangea/widgets/chat/message_toolbar_buttons.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 3976c7505..12c71e736 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -77,6 +77,7 @@ class ToolbarButtons extends StatelessWidget { ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, children: modes.mapIndexed((index, mode) { final enabled = mode.isUnlocked( index, From 25251ae613fae81cff6eee8fb7ad68354d75cacf Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 12:00:10 -0500 Subject: [PATCH 12/23] uncomment button height --- lib/pangea/widgets/pressable_button.dart | 47 +++++++++++------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart index 40d9be423..596511adf 100644 --- a/lib/pangea/widgets/pressable_button.dart +++ b/lib/pangea/widgets/pressable_button.dart @@ -86,33 +86,30 @@ class PressableButtonState extends State onTapDown: _onTapDown, onTapUp: _onTapUp, onTapCancel: _onTapCancel, - child: Container( - decoration: BoxDecoration(border: Border.all(color: Colors.green)), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - Container( - width: widget.width, - // height: widget.height, - decoration: BoxDecoration( - color: Color.alphaBlend( - Colors.black.withOpacity(0.25), - widget.color, - ), - borderRadius: widget.borderRadius, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.black.withOpacity(0.25), + widget.color, ), + borderRadius: widget.borderRadius, ), - AnimatedBuilder( - animation: _tweenAnimation, - builder: (context, _) { - return Positioned( - bottom: widget.depressed ? 0 : _tweenAnimation.value, - child: widget.child, - ); - }, - ), - ], - ), + ), + AnimatedBuilder( + animation: _tweenAnimation, + builder: (context, _) { + return Positioned( + bottom: widget.depressed ? 0 : _tweenAnimation.value, + child: widget.child, + ); + }, + ), + ], ), ); } From f3841fe0ec3f69f2fdeb5567615b1c6740dc8b5b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 15:14:04 -0500 Subject: [PATCH 13/23] don't rely on fixed dimensions to render pressable buttons, animate in opacity/blur change in overlay backdrop --- lib/config/app_config.dart | 1 + lib/pages/chat/chat.dart | 2 +- lib/pangea/utils/overlay.dart | 112 +++++++++++++----- .../chat/message_selection_overlay.dart | 6 +- .../widgets/chat/message_toolbar_buttons.dart | 2 - lib/pangea/widgets/chat/overlay_message.dart | 3 +- lib/pangea/widgets/pressable_button.dart | 56 +++++---- 7 files changed, 120 insertions(+), 62 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index bec1df519..548e0a90a 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -35,6 +35,7 @@ abstract class AppConfig { static const Color activeToggleColor = Color(0xFF33D057); static const Color success = Color(0xFF33D057); static const Color warning = Color.fromARGB(255, 210, 124, 12); + static const int overlayAnimationDuration = 250; // static String _privacyUrl = // 'https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md'; static String _privacyUrl = "https://www.pangeachat.com/privacy"; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 203186116..091b35047 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1699,7 +1699,7 @@ class ChatController extends State context: context, child: overlayEntry, transformTargetId: "", - backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(150), + backgroundColor: Colors.black, closePrevOverlay: MatrixState.pangeaController.subscriptionController.isSubscribed, position: OverlayPositionEnum.centered, diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index ef7a16b80..49ef48851 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:ui'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/common_widgets/overlay_container.dart'; import 'package:flutter/foundation.dart'; @@ -219,7 +220,7 @@ class OverlayUtil { static bool get isOverlayOpen => MatrixState.pAnyState.entries.isNotEmpty; } -class TransparentBackdrop extends StatelessWidget { +class TransparentBackdrop extends StatefulWidget { final Color? backgroundColor; final Function? onDismiss; final bool blurBackground; @@ -232,34 +233,91 @@ class TransparentBackdrop extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Material( - borderOnForeground: false, - color: backgroundColor ?? Colors.transparent, - clipBehavior: Clip.antiAlias, - child: InkWell( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - focusColor: Colors.transparent, - highlightColor: Colors.transparent, - onTap: () { - if (onDismiss != null) { - onDismiss!(); - } - MatrixState.pAnyState.closeOverlay(); - }, - child: BackdropFilter( - filter: blurBackground - ? ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5) - : ImageFilter.blur(sigmaX: 0, sigmaY: 0), - child: Container( - height: double.infinity, - width: double.infinity, - color: Colors.transparent, - ), - ), + TransparentBackdropState createState() => TransparentBackdropState(); +} + +class TransparentBackdropState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _opacityTween; + late Animation _blurTween; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), + vsync: this, + ); + _opacityTween = Tween(begin: 0.0, end: 0.8).animate( + CurvedAnimation( + parent: _controller, + curve: FluffyThemes.animationCurve, ), ); + _blurTween = Tween(begin: 0.0, end: 2.5).animate( + CurvedAnimation( + parent: _controller, + curve: FluffyThemes.animationCurve, + ), + ); + + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) _controller.forward(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _opacityTween, + builder: (context, _) { + return Material( + borderOnForeground: false, + color: widget.backgroundColor?.withOpacity(_opacityTween.value) ?? + Colors.transparent, + clipBehavior: Clip.antiAlias, + child: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + focusColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: () { + if (widget.onDismiss != null) { + widget.onDismiss!(); + } + MatrixState.pAnyState.closeOverlay(); + }, + child: AnimatedBuilder( + animation: _blurTween, + builder: (context, _) { + return BackdropFilter( + filter: widget.blurBackground + ? ImageFilter.blur( + sigmaX: _blurTween.value, + sigmaY: _blurTween.value, + ) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Container( + height: double.infinity, + width: double.infinity, + color: Colors.transparent, + ), + ); + }, + ), + ), + // ), + ); + }, + ); } } diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index baa84ccc7..80263c62f 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -80,7 +80,8 @@ class MessageOverlayController extends State super.initState(); _animationController = AnimationController( vsync: this, - duration: FluffyThemes.animationDuration, + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), ); activitiesLeftToComplete = activitiesLeftToComplete - @@ -372,7 +373,8 @@ class MessageOverlayController extends State widget.chatController.scrollController.animateTo( widget.chatController.scrollController.offset - scrollOffset, - duration: FluffyThemes.animationDuration, + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), curve: FluffyThemes.animationCurve, ); _animationController.forward(); diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 12c71e736..2ddd3c29d 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -94,8 +94,6 @@ class ToolbarButtons extends StatelessWidget { return Tooltip( message: mode.tooltip(context), child: PressableButton( - width: buttonSize, - height: buttonSize, borderRadius: BorderRadius.circular(20), enabled: enabled, depressed: !enabled || mode == overlayController.toolbarMode, diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index c0a362fc3..db6ba8424 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -75,7 +75,8 @@ class OverlayMessage extends StatelessWidget { ); final displayEvent = pangeaMessageEvent.event.getDisplayEvent(timeline); - var color = theme.colorScheme.surfaceContainerHighest; + // ignore: deprecated_member_use + var color = theme.colorScheme.surfaceVariant; if (ownMessage) { color = displayEvent.status.isError ? Colors.redAccent diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart index 596511adf..5c4a32a7d 100644 --- a/lib/pangea/widgets/pressable_button.dart +++ b/lib/pangea/widgets/pressable_button.dart @@ -4,21 +4,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class PressableButton extends StatefulWidget { - final double width; - final double height; final BorderRadius borderRadius; final double buttonHeight; - final bool enabled; final bool depressed; - final Color color; final Widget child; final void Function()? onPressed; const PressableButton({ - required this.width, - required this.height, required this.borderRadius, required this.child, required this.onPressed, @@ -53,6 +47,7 @@ class PressableButtonState extends State void _onTapDown(TapDownDetails details) { if (!widget.enabled) return; _animationCompleter = Completer(); + if (!mounted) return; _controller.forward().then((_) { _animationCompleter?.complete(); _animationCompleter = null; @@ -61,17 +56,17 @@ class PressableButtonState extends State Future _onTapUp(TapUpDetails details) async { if (!widget.enabled) return; + widget.onPressed?.call(); if (_animationCompleter != null) { await _animationCompleter!.future; } - _controller.reverse(); - widget.onPressed?.call(); + if (mounted) _controller.reverse(); HapticFeedback.mediumImpact(); } void _onTapCancel() { if (!widget.enabled) return; - _controller.reverse(); + if (mounted) _controller.reverse(); } @override @@ -86,28 +81,31 @@ class PressableButtonState extends State onTapDown: _onTapDown, onTapUp: _onTapUp, onTapCancel: _onTapCancel, - child: Stack( - alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ - Container( - width: widget.width, - height: widget.height, - decoration: BoxDecoration( - color: Color.alphaBlend( - Colors.black.withOpacity(0.25), - widget.color, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: _tweenAnimation, + builder: (context, _) { + return Container( + padding: EdgeInsets.only(bottom: _tweenAnimation.value), + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.black.withOpacity(0.25), + widget.color, + ), + borderRadius: widget.borderRadius, + ), + child: widget.child, + ); + }, ), - borderRadius: widget.borderRadius, - ), - ), - AnimatedBuilder( - animation: _tweenAnimation, - builder: (context, _) { - return Positioned( - bottom: widget.depressed ? 0 : _tweenAnimation.value, - child: widget.child, - ); - }, + ], ), ], ), From a1675c072c47c2147e90ca652d9b5e366027e4db Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 15:31:25 -0500 Subject: [PATCH 14/23] keep button down if disabled --- lib/pangea/widgets/pressable_button.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart index 5c4a32a7d..54734254c 100644 --- a/lib/pangea/widgets/pressable_button.dart +++ b/lib/pangea/widgets/pressable_button.dart @@ -55,7 +55,7 @@ class PressableButtonState extends State } Future _onTapUp(TapUpDetails details) async { - if (!widget.enabled) return; + if (!widget.enabled || widget.depressed) return; widget.onPressed?.call(); if (_animationCompleter != null) { await _animationCompleter!.future; @@ -93,7 +93,11 @@ class PressableButtonState extends State animation: _tweenAnimation, builder: (context, _) { return Container( - padding: EdgeInsets.only(bottom: _tweenAnimation.value), + padding: EdgeInsets.only( + bottom: widget.enabled && !widget.depressed + ? _tweenAnimation.value + : 0, + ), decoration: BoxDecoration( color: Color.alphaBlend( Colors.black.withOpacity(0.25), From faabe5a903cbf959d3af148b80d13b229451c9aa Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 15:44:03 -0500 Subject: [PATCH 15/23] give bot style the same font as messages, make question in activity cards bot style --- lib/pangea/utils/bot_style.dart | 3 +-- .../widgets/practice_activity/multiple_choice_activity.dart | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/pangea/utils/bot_style.dart b/lib/pangea/utils/bot_style.dart index 1a3a2f8fb..36f9cb85d 100644 --- a/lib/pangea/utils/bot_style.dart +++ b/lib/pangea/utils/bot_style.dart @@ -9,11 +9,10 @@ class BotStyle { bool setColor = true, bool big = false, bool italics = false, - bool bold = true, + bool bold = false, }) { try { final TextStyle botStyle = TextStyle( - fontFamily: 'Inconsolata', fontWeight: bold ? FontWeight.w700 : null, fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor * diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index a477efa9b..d37084283 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.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/bot_style.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; @@ -108,10 +109,7 @@ class MultipleChoiceActivityState extends State { children: [ Text( practiceActivity.content.question, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: BotStyle.text(context), ), const SizedBox(height: 8), if (practiceActivity.activityType == From d0335471282bdfa05eac70fade1f2a8d60ed3675 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 16:20:18 -0500 Subject: [PATCH 16/23] increase minimum dimensions of toolbar --- lib/config/app_config.dart | 4 +- lib/pangea/widgets/chat/message_toolbar.dart | 15 +++++-- .../chat/message_translation_card.dart | 42 +++++++------------ 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 548e0a90a..ef4487e9a 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -23,8 +23,8 @@ abstract class AppConfig { static const bool allowOtherHomeservers = true; static const bool enableRegistration = true; static const double toolbarMaxHeight = 300.0; - static const double toolbarMinHeight = 70.0; - static const double toolbarMinWidth = 270.0; + static const double toolbarMinHeight = 175.0; + static const double toolbarMinWidth = 350.0; static const double toolbarButtonsHeight = 50.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index ffca4f32c..92d4b578c 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -138,10 +138,17 @@ class MessageToolbar extends StatelessWidget { minHeight: AppConfig.toolbarMinHeight, // maxWidth is set by MessageSelectionOverlay ), - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: toolbarContent(context), + child: Container( + decoration: BoxDecoration(border: Border.all(color: Colors.green)), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: toolbarContent(context), + ), + ], ), ), ); diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 3c0d750a3..0f95a3e55 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -147,35 +147,25 @@ class MessageTranslationCardState extends State { return Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), - child: Row( - mainAxisSize: MainAxisSize.min, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.selection != null - ? selectionTranslation! - : repEvent!.text, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ), - if (notGoingToTranslate && widget.selection == null) - InlineTooltip( - instructionsEnum: InstructionsEnum.l1Translation, - onClose: () => setState(() {}), - ), - if (widget.selection != null) - InlineTooltip( - instructionsEnum: InstructionsEnum.clickAgainToDeselect, - onClose: () => setState(() {}), - ), - ], - ), + Text( + widget.selection != null ? selectionTranslation! : repEvent!.text, + style: BotStyle.text(context), + textAlign: TextAlign.center, ), + if (notGoingToTranslate && widget.selection == null) + InlineTooltip( + instructionsEnum: InstructionsEnum.l1Translation, + onClose: () => setState(() {}), + ), + if (widget.selection != null) + InlineTooltip( + instructionsEnum: InstructionsEnum.clickAgainToDeselect, + onClose: () => setState(() {}), + ), ], ), ); From c5ffa0e03798248d35eaa698324fb206ed108398 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 16:25:47 -0500 Subject: [PATCH 17/23] removed toolbar border and added space between between overlay message and toolbar --- .../chat/message_selection_overlay.dart | 1 + lib/pangea/widgets/chat/message_toolbar.dart | 25 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 80263c62f..f6a266f95 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -491,6 +491,7 @@ class MessageOverlayController extends State overLayController: this, tts: tts, ), + const SizedBox(height: 8), SizedBox( height: adjustedMessageHeight, child: OverlayMessage( diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 92d4b578c..79561ef06 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -124,10 +124,6 @@ class MessageToolbar extends StatelessWidget { return Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, - border: Border.all( - width: 2, - color: Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), @@ -138,18 +134,15 @@ class MessageToolbar extends StatelessWidget { minHeight: AppConfig.toolbarMinHeight, // maxWidth is set by MessageSelectionOverlay ), - child: Container( - decoration: BoxDecoration(border: Border.all(color: Colors.green)), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedSize( - duration: FluffyThemes.animationDuration, - child: toolbarContent(context), - ), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: toolbarContent(context), + ), + ], ), ); } From c297dea4375a847555ca75545c58d0558dda0a57 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 6 Nov 2024 20:25:30 -0500 Subject: [PATCH 18/23] some questions, name changes, and a couple switches from grammar to morph uses --- lib/main.dart | 2 +- lib/pages/chat/chat.dart | 17 +- lib/pages/chat/chat_view.dart | 2 +- lib/pages/chat_list/chat_list.dart | 5 +- .../controllers/it_controller.dart | 4 +- lib/pangea/choreographer/widgets/it_bar.dart | 4 +- .../controllers/get_analytics_controller.dart | 18 +- .../message_analytics_controller.dart | 535 ---------------- lib/pangea/controllers/pangea_controller.dart | 44 +- ...ler.dart => put_analytics_controller.dart} | 69 ++- lib/pangea/enum/construct_type_enum.dart | 15 +- .../pangea_room_extension.dart | 8 +- ... room_children_and_parents_extension.dart} | 0 ...ension.dart => room_events_extension.dart} | 64 -- ...art => room_space_settings_extension.dart} | 0 ...t => room_user_permissions_extension.dart} | 0 .../pangea_message_event.dart | 31 - .../models/analytics/constructs_model.dart | 50 +- lib/pangea/models/choreo_record.dart | 42 -- .../models/representation_content_model.dart | 4 +- .../pages/analytics/construct_list.dart | 572 ------------------ .../analytics/list_summary_analytics.dart | 101 ---- lib/pangea/utils/logout.dart | 2 +- .../widgets/animations/gain_points.dart | 8 +- .../learning_progress_indicators.dart | 16 +- lib/pangea/widgets/igc/span_card.dart | 6 +- .../multiple_choice_activity.dart | 4 +- .../practice_activity_card.dart | 2 +- .../target_tokens_controller.dart | 2 +- .../word_focus_listening_activity.dart | 4 +- .../user_settings/p_language_dialog.dart | 15 +- 31 files changed, 133 insertions(+), 1513 deletions(-) delete mode 100644 lib/pangea/controllers/message_analytics_controller.dart rename lib/pangea/controllers/{my_analytics_controller.dart => put_analytics_controller.dart} (86%) rename lib/pangea/extensions/pangea_room_extension/{children_and_parents_extension.dart => room_children_and_parents_extension.dart} (100%) rename lib/pangea/extensions/pangea_room_extension/{events_extension.dart => room_events_extension.dart} (83%) rename lib/pangea/extensions/pangea_room_extension/{space_settings_extension.dart => room_space_settings_extension.dart} (100%) rename lib/pangea/extensions/pangea_room_extension/{user_permissions_extension.dart => room_user_permissions_extension.dart} (100%) delete mode 100644 lib/pangea/pages/analytics/construct_list.dart delete mode 100644 lib/pangea/pages/analytics/list_summary_analytics.dart diff --git a/lib/main.dart b/lib/main.dart index 9f5e656bd..847b012b2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,7 +23,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env"); + await dotenv.load(fileName: ".env.local_choreo"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 091b35047..071c5f10c 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -15,8 +15,8 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/message_mode_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'; @@ -679,18 +679,15 @@ class ChatController extends State ); if (msgEventId != null) { - pangeaController.myAnalytics.setState( + pangeaController.putAnalytics.setState( AnalyticsStream( eventId: msgEventId, roomId: room.id, - constructs: [ - ...(choreo!.grammarConstructUses(metadata: metadata)), - ...(originalSent!.vocabUses( - choreo: choreo, - tokens: tokensSent!.tokens, - metadata: metadata, - )), - ], + constructs: originalSent!.vocabUses( + choreo: choreo, + tokens: tokensSent!.tokens, + metadata: metadata, + ), origin: AnalyticsUpdateOrigin.sendMessage, ), ); diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index e3df6a0e2..7b2a3cf2b 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart'; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 2d1302993..770d9204f 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -1016,8 +1016,9 @@ class ChatListController extends State } // #Pangea - MatrixState.pangeaController.myAnalytics.initialize(); - MatrixState.pangeaController.analytics.initialize(); + //@ggurdin why is are these two initialized separately? why not in the _initPangeaControllers? + MatrixState.pangeaController.putAnalytics.initialize(); + MatrixState.pangeaController.getAnalytics.initialize(); await _initPangeaControllers(client); // Pangea# if (!mounted) return; diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index fce7c79ed..f2d3e1ed4 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/edit_type.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; @@ -318,7 +318,7 @@ class ITController { .toList(); // Save those choices' tokens to local construct analytics as ignored tokens - choreographer.pangeaController.myAnalytics.addDraftUses( + choreographer.pangeaController.putAnalytics.addDraftUses( ignoredTokens ?? [], choreographer.roomId, ConstructUseTypeEnum.ignIt, diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 76050b226..ff7ae8d31 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -7,7 +7,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart'; import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -369,7 +369,7 @@ class ITChoices extends StatelessWidget { ); } if (!continuance.wasClicked) { - controller.choreographer.pangeaController.myAnalytics.addDraftUses( + controller.choreographer.pangeaController.putAnalytics.addDraftUses( continuance.tokens, controller.choreographer.roomId, continuance.level > 1 diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index 1f891ccd9..dba450091 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -4,8 +4,8 @@ import 'dart:math'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/match_rule_ids.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; @@ -70,10 +70,10 @@ class GetAnalyticsController { void initialize() { _analyticsUpdateSubscription ??= _pangeaController - .myAnalytics.analyticsUpdateStream.stream + .putAnalytics.analyticsUpdateStream.stream .listen(onAnalyticsUpdate); - _pangeaController.myAnalytics.lastUpdatedCompleter.future.then((_) { + _pangeaController.putAnalytics.lastUpdatedCompleter.future.then((_) { getConstructs().then((_) => updateAnalyticsStream()); }); } @@ -127,9 +127,10 @@ class GetAnalyticsController { uses: constructs, type: ConstructTypeEnum.vocab, ); + final errors = ConstructListModel( uses: constructs, - type: ConstructTypeEnum.grammar, + type: ConstructTypeEnum.morph, ); return words.points + errors.points; } @@ -168,7 +169,7 @@ class GetAnalyticsController { return formattedCache; } catch (err) { // if something goes wrong while trying to format the local data, clear it - _pangeaController.myAnalytics + _pangeaController.putAnalytics .clearMessagesSinceUpdate(clearDrafts: true); return {}; } @@ -205,7 +206,7 @@ class GetAnalyticsController { await client.roomsLoading; // don't try to get constructs until last updated time has been loaded - await _pangeaController.myAnalytics.lastUpdatedCompleter.future; + await _pangeaController.putAnalytics.lastUpdatedCompleter.future; // if forcing a refreshing, clear the cache if (forceUpdate) _cache.clear(); @@ -273,6 +274,9 @@ class GetAnalyticsController { /// Filter out constructs that are not relevant to the user, specifically those from /// rooms in which the user is a teacher and those that are interative translation span constructs + /// @ggurdin - is this still relevant now that we're not doing grammar constructs? + /// maybe it should actually be filtering all grammar uses, though this is maybe more efficiently done + /// in the fromJson of the model reading the event content, then maybe we can get rid of that enum entry entirely Future> filterConstructs({ required List unfilteredConstructs, }) async { @@ -295,7 +299,7 @@ class GetAnalyticsController { ); if (index > -1) { - final DateTime? lastUpdated = _pangeaController.myAnalytics.lastUpdated; + final DateTime? lastUpdated = _pangeaController.putAnalytics.lastUpdated; if (_cache[index].needsUpdate(lastUpdated)) { _cache.removeAt(index); return null; diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart deleted file mode 100644 index 29cbef9ea..000000000 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ /dev/null @@ -1,535 +0,0 @@ -import 'dart:async'; - -import 'package:fluffychat/pangea/constants/match_rule_ids.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; -import 'package:fluffychat/pangea/enum/time_span.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; -import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:flutter/foundation.dart'; -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -import '../constants/class_default_values.dart'; -import '../extensions/client_extension/client_extension.dart'; -import '../extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'base_controller.dart'; -import 'pangea_controller.dart'; - -// controls the fetching of analytics data -class AnalyticsController extends BaseController { - late PangeaController _pangeaController; - final List _cachedConstructs = []; - - AnalyticsController(PangeaController pangeaController) : super() { - _pangeaController = pangeaController; - } - - String get langCode => - _pangeaController.languageController.userL2?.langCode ?? - _pangeaController.pLanguageStore.targetOptions.first.langCode; - - // String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY"; - - // TimeSpan get currentAnalyticsTimeSpan { - // try { - // final String? str = _pangeaController.pStoreService.read( - // _analyticsTimeSpanKey, - // ); - // return str != null - // ? TimeSpan.values.firstWhere((e) { - // final spanString = e.toString(); - // return spanString == str; - // }) - // : ClassDefaultValues.defaultTimeSpan; - // } catch (err) { - // debugger(when: kDebugMode); - // return ClassDefaultValues.defaultTimeSpan; - // } - // } - - // Future setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async { - // await _pangeaController.pStoreService.save( - // _analyticsTimeSpanKey, - // timeSpan.toString(), - // ); - // setState(); - // } - - // String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY"; - - // LanguageModel get currentAnalyticsLang { - // try { - // final String? str = _pangeaController.pStoreService.read( - // _analyticsSpaceLangKey, - // ); - // return str != null - // ? PangeaLanguage.byLangCode(str) - // : _pangeaController.languageController.userL2 ?? - // _pangeaController.pLanguageStore.targetOptions.first; - // } catch (err) { - // debugger(when: kDebugMode); - // return _pangeaController.pLanguageStore.targetOptions.first; - // } - // } - - // Future setCurrentAnalyticsLang(LanguageModel lang) async { - // await _pangeaController.pStoreService.save( - // _analyticsSpaceLangKey, - // lang.langCode, - // ); - // setState(); - // } - - /// Get the last time the user updated their analytics. - /// Tries to get the last time the user updated analytics for their current L2. - /// If there isn't yet an analytics room reacted for their L2, checks if the - /// user has any other analytics rooms and returns the most recent update time. - Future myAnalyticsLastUpdated() async { - final List analyticsRooms = - _pangeaController.matrixState.client.allMyAnalyticsRooms; - - final Map langCodeLastUpdates = {}; - for (final Room analyticsRoom in analyticsRooms) { - final String? roomLang = analyticsRoom.madeForLang; - if (roomLang == null) continue; - final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated( - _pangeaController.matrixState.client.userID!, - ); - if (lastUpdated != null) { - langCodeLastUpdates[roomLang] = lastUpdated; - } - } - - if (langCodeLastUpdates.isEmpty) return null; - final String? l2Code = - _pangeaController.languageController.userL2?.langCode; - if (l2Code != null && langCodeLastUpdates.containsKey(l2Code)) { - return langCodeLastUpdates[l2Code]; - } - return langCodeLastUpdates.values.reduce( - (check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent, - ); - } - - /// check if any students have recently updated their analytics - /// if any have, then the cache needs to be updated - Future spaceAnalyticsLastUpdated( - Room space, - ) async { - await space.requestParticipants(); - - final List> lastUpdatedFutures = []; - for (final student in space.students) { - final Room? analyticsRoom = _pangeaController.matrixState.client - .analyticsRoomLocal(langCode, student.id); - if (analyticsRoom == null) continue; - lastUpdatedFutures.add( - analyticsRoom.analyticsLastUpdated(student.id), - ); - } - - final List lastUpdatedWithNulls = - await Future.wait(lastUpdatedFutures); - final List lastUpdates = - lastUpdatedWithNulls.where((e) => e != null).cast().toList(); - if (lastUpdates.isNotEmpty) { - return lastUpdates.reduce( - (check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent, - ); - } - return null; - } - - Future> allMyConstructs( - TimeSpan timeSpan, - ) async { - final Room? analyticsRoom = - _pangeaController.matrixState.client.analyticsRoomLocal(langCode); - if (analyticsRoom == null) return []; - - final List? roomEvents = - (await analyticsRoom.getAnalyticsEvents( - since: timeSpan.cutOffDate, - userId: _pangeaController.matrixState.client.userID!, - )) - ?.cast(); - final List allConstructs = roomEvents ?? []; - - return allConstructs - .where((construct) => construct.content.uses.isNotEmpty) - .toList(); - } - - Future> allSpaceMemberConstructs( - Room space, - TimeSpan timeSpan, - ) async { - await space.requestParticipants(); - final List constructEvents = []; - for (final student in space.students) { - final Room? analyticsRoom = _pangeaController.matrixState.client - .analyticsRoomLocal(langCode, student.id); - if (analyticsRoom != null) { - final List? roomEvents = - (await analyticsRoom.getAnalyticsEvents( - since: timeSpan.cutOffDate, - userId: student.id, - )) - ?.cast(); - constructEvents.addAll(roomEvents ?? []); - } - } - - final List spaceChildrenIds = space.allSpaceChildRoomIds; - final List allConstructs = []; - for (final constructEvent in constructEvents) { - constructEvent.content.uses.removeWhere( - (use) => !spaceChildrenIds.contains(use.chatId), - ); - - if (constructEvent.content.uses.isNotEmpty) { - allConstructs.add(constructEvent); - } - } - - return allConstructs; - } - - List filterStudentConstructs( - List unfilteredConstructs, - String? studentId, - ) { - final List filtered = - List.from(unfilteredConstructs); - filtered.removeWhere((element) => element.event.senderId != studentId); - return filtered; - } - - List filterRoomConstructs( - List unfilteredConstructs, - String? roomID, - ) { - final List filtered = [...unfilteredConstructs]; - for (final construct in filtered) { - construct.content.uses.removeWhere((u) => u.chatId != roomID); - } - return filtered; - } - - Future> filterPrivateChatConstructs( - List unfilteredConstructs, - Room space, - ) async { - final List privateChatIds = space.allSpaceChildRoomIds; - final resp = await space.client.getSpaceHierarchy(space.id); - final List chatIds = resp.rooms.map((room) => room.roomId).toList(); - for (final id in chatIds) { - privateChatIds.removeWhere((e) => e == id); - } - final List filtered = - List.from(unfilteredConstructs); - for (final construct in filtered) { - construct.content.uses.removeWhere( - (use) => !privateChatIds.contains(use.chatId), - ); - } - return filtered; - } - - Future> filterSpaceConstructs( - List unfilteredConstructs, - Room space, - ) async { - final resp = await space.client.getSpaceHierarchy(space.id); - final List chatIds = resp.rooms.map((room) => room.roomId).toList(); - final List filtered = - List.from(unfilteredConstructs); - - for (final construct in filtered) { - construct.content.uses.removeWhere( - (use) => !chatIds.contains(use.chatId), - ); - } - - return filtered; - } - - List? getConstructsLocal({ - required TimeSpan timeSpan, - required AnalyticsSelected defaultSelected, - AnalyticsSelected? selected, - DateTime? lastUpdated, - ConstructTypeEnum? constructType, - }) { - final index = _cachedConstructs.indexWhere( - (e) => - e.timeSpan == timeSpan && - e.type == constructType && - e.defaultSelected.id == defaultSelected.id && - e.defaultSelected.type == defaultSelected.type && - e.selected?.id == selected?.id && - e.selected?.type == selected?.type && - e.langCode == langCode, - ); - - if (index > -1) { - if (_cachedConstructs[index].needsUpdate(lastUpdated)) { - _cachedConstructs.removeAt(index); - return null; - } - return _cachedConstructs[index].events; - } - - return null; - } - - void cacheConstructs({ - required List events, - required AnalyticsSelected defaultSelected, - required TimeSpan timeSpan, - AnalyticsSelected? selected, - ConstructTypeEnum? constructType, - }) { - final entry = ConstructCacheEntry( - timeSpan: timeSpan, - type: constructType, - events: List.from(events), - defaultSelected: defaultSelected, - selected: selected, - langCode: langCode, - ); - _cachedConstructs.add(entry); - } - - Future> getMyConstructs({ - required AnalyticsSelected defaultSelected, - required TimeSpan timeSpan, - ConstructTypeEnum? constructType, - AnalyticsSelected? selected, - }) async { - final List unfilteredConstructs = - await allMyConstructs(timeSpan); - - final Room? space = selected?.type == AnalyticsEntryType.space - ? _pangeaController.matrixState.client.getRoomById(selected!.id) - : null; - - return filterConstructs( - unfilteredConstructs: unfilteredConstructs, - space: space, - defaultSelected: defaultSelected, - selected: selected, - timeSpan: timeSpan, - ); - } - - Future> getSpaceConstructs({ - required Room space, - required AnalyticsSelected defaultSelected, - required TimeSpan timeSpan, - AnalyticsSelected? selected, - ConstructTypeEnum? constructType, - }) async { - final List unfilteredConstructs = - await allSpaceMemberConstructs( - space, - timeSpan, - ); - - return filterConstructs( - unfilteredConstructs: unfilteredConstructs, - space: space, - defaultSelected: defaultSelected, - selected: selected, - timeSpan: timeSpan, - ); - } - - Future> filterConstructs({ - required List unfilteredConstructs, - required AnalyticsSelected defaultSelected, - required TimeSpan timeSpan, - Room? space, - AnalyticsSelected? selected, - }) async { - if ([AnalyticsEntryType.privateChats, AnalyticsEntryType.space] - .contains(selected?.type)) { - assert(space != null); - } - - for (int i = 0; i < unfilteredConstructs.length; i++) { - final construct = unfilteredConstructs[i]; - construct.content.uses.removeWhere( - (use) => use.timeStamp.isBefore(timeSpan.cutOffDate), - ); - } - - unfilteredConstructs.removeWhere((e) => e.content.uses.isEmpty); - - switch (selected?.type) { - case null: - return unfilteredConstructs; - case AnalyticsEntryType.student: - if (defaultSelected.type != AnalyticsEntryType.space) { - throw Exception( - "student filtering not available for default filter ${defaultSelected.type}", - ); - } - return filterStudentConstructs(unfilteredConstructs, selected!.id); - case AnalyticsEntryType.room: - return filterRoomConstructs(unfilteredConstructs, selected?.id); - case AnalyticsEntryType.privateChats: - return defaultSelected.type == AnalyticsEntryType.student - ? throw "private chat filtering not available for my analytics" - : await filterPrivateChatConstructs(unfilteredConstructs, space!); - case AnalyticsEntryType.space: - return await filterSpaceConstructs(unfilteredConstructs, space!); - default: - throw Exception("invalid filter type - ${selected?.type}"); - } - } - - Future?> getConstructs({ - required AnalyticsSelected defaultSelected, - required TimeSpan timeSpan, - AnalyticsSelected? selected, - bool removeIT = true, - bool forceUpdate = false, - ConstructTypeEnum? constructType, - }) async { - debugPrint("getting constructs"); - await _pangeaController.matrixState.client.roomsLoading; - - Room? space; - if (defaultSelected.type == AnalyticsEntryType.space) { - space = _pangeaController.matrixState.client.getRoomById( - defaultSelected.id, - ); - if (space == null) { - ErrorHandler.logError( - m: "space not found in setConstructs", - data: { - "defaultSelected": defaultSelected, - "selected": selected, - }, - ); - return []; - } - } - - DateTime? lastUpdated; - if (defaultSelected.type != AnalyticsEntryType.space) { - // if default selected view is my analytics, check for the last - // time the logged in user updated their analytics events - // this gets passed to getAnalyticsLocal to determine if the cached - // entry is out-of-date - lastUpdated = await myAnalyticsLastUpdated(); - } else { - // else, get the last time a student in the space updated their analytics - lastUpdated = await spaceAnalyticsLastUpdated( - space!, - ); - } - - final List? local = getConstructsLocal( - timeSpan: timeSpan, - constructType: constructType, - defaultSelected: defaultSelected, - selected: selected, - lastUpdated: lastUpdated, - ); - if (local != null && !forceUpdate) { - debugPrint("returning local constructs"); - return local; - } - debugPrint("fetching new constructs"); - - final filteredConstructs = space == null - ? await getMyConstructs( - constructType: constructType, - defaultSelected: defaultSelected, - selected: selected, - timeSpan: timeSpan, - ) - : await getSpaceConstructs( - constructType: constructType, - space: space, - defaultSelected: defaultSelected, - selected: selected, - timeSpan: timeSpan, - ); - - if (removeIT) { - for (final construct in filteredConstructs) { - construct.content.uses.removeWhere( - (element) => - element.lemma == "Try interactive translation" || - element.lemma == "itStart" || - element.lemma == MatchRuleIds.interactiveTranslation, - ); - } - } - - if (local == null) { - cacheConstructs( - constructType: constructType, - events: filteredConstructs, - defaultSelected: defaultSelected, - selected: selected, - timeSpan: timeSpan, - ); - } - - return filteredConstructs; - } -} - -abstract class CacheEntry { - final String langCode; - final TimeSpan timeSpan; - final AnalyticsSelected defaultSelected; - AnalyticsSelected? selected; - late final DateTime _createdAt; - - CacheEntry({ - required this.timeSpan, - required this.defaultSelected, - required this.langCode, - this.selected, - }) { - _createdAt = DateTime.now(); - } - - bool get isExpired => - DateTime.now().difference(_createdAt).inMinutes > - ClassDefaultValues.minutesDelayToMakeNewChartAnalytics; - - bool needsUpdate(DateTime? lastEventUpdated) { - // cache entry is invalid if it's older than the last event update - // if lastEventUpdated is null, that would indicate that no events - // of this type have been sent to the room. In this case, there - // shouldn't be any cached data. - if (lastEventUpdated == null) { - Sentry.addBreadcrumb( - Breadcrumb(message: "lastEventUpdated is null in needsUpdate"), - ); - return false; - } - return _createdAt.isBefore(lastEventUpdated); - } -} - -class ConstructCacheEntry extends CacheEntry { - final ConstructTypeEnum? type; - final List events; - - ConstructCacheEntry({ - required this.events, - required super.timeSpan, - required super.langCode, - required super.defaultSelected, - this.type, - super.selected, - }); -} diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index c24e8b3d2..e0b33b054 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -12,10 +12,10 @@ import 'package:fluffychat/pangea/controllers/language_controller.dart'; import 'package:fluffychat/pangea/controllers/language_detection_controller.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.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/put_analytics_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'; @@ -44,11 +44,12 @@ class PangeaController { late LanguageController languageController; late ClassController classController; late PermissionsController permissionsController; - // late AnalyticsController analytics; - late GetAnalyticsController analytics; - late MyAnalyticsController myAnalytics; + late GetAnalyticsController getAnalytics; + late PutAnalyticsController putAnalytics; late WordController wordNet; late MessageDataController messageData; + + // TODO: make these static so we can remove from here late ContextualDefinitionController definitions; late ITFeedbackController itFeedback; late InstructionsController instructions; @@ -93,9 +94,8 @@ class PangeaController { languageController = LanguageController(this); classController = ClassController(this); permissionsController = PermissionsController(this); - // analytics = AnalyticsController(this); - analytics = GetAnalyticsController(this); - myAnalytics = MyAnalyticsController(this); + getAnalytics = GetAnalyticsController(this); + putAnalytics = PutAnalyticsController(this); messageData = MessageDataController(this); wordNet = WordController(this); definitions = ContextualDefinitionController(this); @@ -146,13 +146,13 @@ class PangeaController { case LoginState.loggedOut: case LoginState.softLoggedOut: // Reset cached analytics data - MatrixState.pangeaController.myAnalytics.dispose(); - MatrixState.pangeaController.analytics.dispose(); + MatrixState.pangeaController.putAnalytics.dispose(); + MatrixState.pangeaController.getAnalytics.dispose(); break; case LoginState.loggedIn: // Initialize analytics data - MatrixState.pangeaController.myAnalytics.initialize(); - MatrixState.pangeaController.analytics.initialize(); + MatrixState.pangeaController.putAnalytics.initialize(); + MatrixState.pangeaController.getAnalytics.initialize(); break; } if (state != LoginState.loggedIn) { @@ -169,28 +169,6 @@ class PangeaController { GoogleAnalytics.analyticsUserUpdate(matrixState.client.userID); } - // void startChatWithBotIfNotPresent() { - // Future.delayed(const Duration(milliseconds: 5000), () async { - // try { - // if (pStoreService.read("started_bot_chat", addClientIdToKey: false) ?? - // false) { - // return; - // } - // await pStoreService.save("started_bot_chat", true, - // addClientIdToKey: false); - // final rooms = matrixState.client.rooms; - - // await matrixState.client.startDirectChat( - // BotName.byEnvironment, - // enableEncryption: false, - // ); - // } catch (err, stack) { - // debugger(when: kDebugMode); - // ErrorHandler.logError(e: err, s: stack); - // } - // }); - // } - void startChatWithBotIfNotPresent() { Future.delayed(const Duration(milliseconds: 10000), () async { // check if user is logged in diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/put_analytics_controller.dart similarity index 86% rename from lib/pangea/controllers/my_analytics_controller.dart rename to lib/pangea/controllers/put_analytics_controller.dart index 9c7523991..37768cdf0 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/put_analytics_controller.dart @@ -19,7 +19,7 @@ enum AnalyticsUpdateType { server, local } /// handles the processing of analytics for /// 1) messages sent by the user and /// 2) constructs used by the user, both in sending messages and doing practice activities -class MyAnalyticsController extends BaseController { +class PutAnalyticsController extends BaseController { late PangeaController _pangeaController; CachedStreamController analyticsUpdateStream = CachedStreamController(); @@ -47,13 +47,13 @@ class MyAnalyticsController extends BaseController { /// the time since the last update that will trigger an automatic update final Duration _timeSinceUpdate = const Duration(days: 1); - MyAnalyticsController(PangeaController pangeaController) { + PutAnalyticsController(PangeaController pangeaController) { _pangeaController = pangeaController; } void initialize() { - // Listen to a stream that provides the eventIDs - // of new messages sent by the logged in user + // Listen for calls to setState on the analytics stream + // and update the analytics room if necessary _analyticsStream ??= stateStream.listen((data) => _onNewAnalyticsData(data)); @@ -79,7 +79,7 @@ class MyAnalyticsController extends BaseController { try { // if lastUpdated hasn't been set yet, set it lastUpdated ??= - await _pangeaController.analytics.myAnalyticsLastUpdated(); + await _pangeaController.getAnalytics.myAnalyticsLastUpdated(); } catch (err, s) { ErrorHandler.logError( s: s, @@ -100,8 +100,9 @@ class MyAnalyticsController extends BaseController { } } - /// Given the data from a newly sent message, format and cache - /// the message's construct data locally and reset the update timer + /// Given new construct uses, format and cache + /// the data locally and reset the update timer + /// Decide whether to update the analytics room void _onNewAnalyticsData(AnalyticsStream data) { final List constructs = _getDraftUses(data.roomId); @@ -110,25 +111,24 @@ class MyAnalyticsController extends BaseController { final String eventID = data.eventId; final String roomID = data.roomId; - _pangeaController.analytics - .filterConstructs(unfilteredConstructs: constructs) - .then((filtered) { - for (final use in filtered) { + if (kDebugMode) { + for (final use in constructs) { debugPrint( "_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", ); } - if (filtered.isEmpty) return; + } - final level = _pangeaController.analytics.level; + if (constructs.isEmpty) return; - _addLocalMessage(eventID, filtered).then( - (_) { - _clearDraftUses(roomID); - _decideWhetherToUpdateAnalyticsRoom(level, data.origin); - }, - ); - }); + final level = _pangeaController.getAnalytics.level; + + _addLocalMessage(eventID, constructs).then( + (_) { + _clearDraftUses(roomID); + _decideWhetherToUpdateAnalyticsRoom(level, data.origin); + }, + ); } void addDraftUses( @@ -142,8 +142,12 @@ class MyAnalyticsController extends BaseController { timeStamp: DateTime.now(), ); - final uses = tokens - .where((token) => token.lemma.saveVocab) + // we only save those with saveVocab == true + final tokensToSave = + tokens.where((token) => token.lemma.saveVocab).toList(); + + // get all our vocab constructs + final uses = tokensToSave .map( (token) => OneConstructUse( useType: useType, @@ -155,7 +159,8 @@ class MyAnalyticsController extends BaseController { ) .toList(); - for (final token in tokens) { + // get all our grammar constructs + for (final token in tokensToSave) { for (final entry in token.morph.entries) { uses.add( OneConstructUse( @@ -177,19 +182,19 @@ class MyAnalyticsController extends BaseController { } } - final level = _pangeaController.analytics.level; + final level = _pangeaController.getAnalytics.level; _addLocalMessage('draft$roomID', uses).then( (_) => _decideWhetherToUpdateAnalyticsRoom(level, origin), ); } List _getDraftUses(String roomID) { - final currentCache = _pangeaController.analytics.messagesSinceUpdate; + final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate; return currentCache['draft$roomID'] ?? []; } void _clearDraftUses(String roomID) { - final currentCache = _pangeaController.analytics.messagesSinceUpdate; + final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate; currentCache.remove('draft$roomID'); _setMessagesSinceUpdate(currentCache); } @@ -201,7 +206,7 @@ class MyAnalyticsController extends BaseController { List constructs, ) async { try { - final currentCache = _pangeaController.analytics.messagesSinceUpdate; + final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate; constructs.addAll(currentCache[cacheKey] ?? []); currentCache[cacheKey] = constructs; @@ -231,14 +236,14 @@ class MyAnalyticsController extends BaseController { sendLocalAnalyticsToAnalyticsRoom(); }); - if (_pangeaController.analytics.messagesSinceUpdate.length > + if (_pangeaController.getAnalytics.messagesSinceUpdate.length > _maxMessagesCached) { debugPrint("reached max messages, updating"); sendLocalAnalyticsToAnalyticsRoom(); return; } - final int newLevel = _pangeaController.analytics.level; + final int newLevel = _pangeaController.getAnalytics.level; newLevel > prevLevel ? sendLocalAnalyticsToAnalyticsRoom() : analyticsUpdateStream.add( @@ -253,7 +258,7 @@ class MyAnalyticsController extends BaseController { return; } - final localCache = _pangeaController.analytics.messagesSinceUpdate; + final localCache = _pangeaController.getAnalytics.messagesSinceUpdate; final draftKeys = localCache.keys.where((key) => key.startsWith('draft')); if (draftKeys.isEmpty) { _pangeaController.pStoreService.delete(PLocalKey.messagesSinceUpdate); @@ -328,7 +333,7 @@ class MyAnalyticsController extends BaseController { /// The analytics room is determined based on the user's current target language. Future _updateAnalytics() async { // if there's no cached construct data, there's nothing to send - final cachedConstructs = _pangeaController.analytics.messagesSinceUpdate; + final cachedConstructs = _pangeaController.getAnalytics.messagesSinceUpdate; final bool onlyDraft = cachedConstructs.length == 1 && cachedConstructs.keys.single.startsWith('draft'); if (cachedConstructs.isEmpty || onlyDraft) return; @@ -341,7 +346,7 @@ class MyAnalyticsController extends BaseController { // and send cached analytics data to the room await analyticsRoom?.sendConstructsEvent( - _pangeaController.analytics.locallyCachedSentConstructs, + _pangeaController.getAnalytics.locallyCachedSentConstructs, ); } } diff --git a/lib/pangea/enum/construct_type_enum.dart b/lib/pangea/enum/construct_type_enum.dart index 2b346b235..fe70d27ab 100644 --- a/lib/pangea/enum/construct_type_enum.dart +++ b/lib/pangea/enum/construct_type_enum.dart @@ -1,16 +1,19 @@ +import 'dart:developer'; + import 'package:fluffychat/pangea/constants/analytics_constants.dart'; +import 'package:flutter/foundation.dart'; enum ConstructTypeEnum { - grammar, + /// for vocabulary words vocab, + + /// for morphs, actually called "Grammar" in the UI... :P morph, } extension ConstructExtension on ConstructTypeEnum { String get string { switch (this) { - case ConstructTypeEnum.grammar: - return 'grammar'; case ConstructTypeEnum.vocab: return 'vocab'; case ConstructTypeEnum.morph: @@ -20,8 +23,6 @@ extension ConstructExtension on ConstructTypeEnum { int get maxXPPerLemma { switch (this) { - case ConstructTypeEnum.grammar: - return 0; case ConstructTypeEnum.vocab: return AnalyticsConstants.vocabUseMaxXP; case ConstructTypeEnum.morph: @@ -33,9 +34,6 @@ extension ConstructExtension on ConstructTypeEnum { class ConstructTypeUtil { static ConstructTypeEnum fromString(String? string) { switch (string) { - case 'g': - case 'grammar': - return ConstructTypeEnum.grammar; case 'v': case 'vocab': return ConstructTypeEnum.vocab; @@ -43,6 +41,7 @@ class ConstructTypeUtil { case 'morph': return ConstructTypeEnum.morph; default: + debugger(when: kDebugMode); return ConstructTypeEnum.vocab; } } diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 162b4f238..2d631ceb7 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -37,13 +37,13 @@ import '../../models/choreo_record.dart'; import '../../models/representation_content_model.dart'; import '../client_extension/client_extension.dart'; -part "children_and_parents_extension.dart"; -part "events_extension.dart"; part "room_analytics_extension.dart"; +part "room_children_and_parents_extension.dart"; +part "room_events_extension.dart"; part "room_information_extension.dart"; part "room_settings_extension.dart"; -part "space_settings_extension.dart"; -part "user_permissions_extension.dart"; +part "room_space_settings_extension.dart"; +part "room_user_permissions_extension.dart"; extension PangeaRoom on Room { // analytics diff --git a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_children_and_parents_extension.dart similarity index 100% rename from lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart rename to lib/pangea/extensions/pangea_room_extension/room_children_and_parents_extension.dart diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_events_extension.dart similarity index 83% rename from lib/pangea/extensions/pangea_room_extension/events_extension.dart rename to lib/pangea/extensions/pangea_room_extension/room_events_extension.dart index d8e2545ff..10435e6cd 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_events_extension.dart @@ -282,70 +282,6 @@ extension EventsRoomExtension on Room { ); } - // ConstructEvent? _vocabEventLocal(String lemma) { - // if (!isAnalyticsRoom) throw Exception("not an analytics room"); - - // final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma); - - // return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null; - // } - - // Future _vocabEvent( - // String lemma, - // ConstructType type, [ - // bool makeIfNull = false, - // ]) async { - // try { - // if (!isAnalyticsRoom) throw Exception("not an analytics room"); - - // ConstructEvent? localEvent = _vocabEventLocal(lemma); - - // if (localEvent != null) return localEvent; - - // await postLoad(); - // localEvent = _vocabEventLocal(lemma); - - // if (localEvent == null && isRoomOwner && makeIfNull) { - // final Event matrixEvent = await _createVocabEvent(lemma, type); - // localEvent = ConstructEvent(event: matrixEvent); - // } - - // return localEvent!; - // } catch (err) { - // debugger(when: kDebugMode); - // rethrow; - // } - // } - - // Future _createVocabEvent(String lemma, ConstructType type) async { - // try { - // if (!isRoomOwner) { - // throw Exception( - // "Tried to create vocab event in room where user is not owner", - // ); - // } - // final String eventId = await client.setRoomStateWithKey( - // id, - // PangeaEventTypes.vocab, - // lemma, - // ConstructUses(lemma: lemma, type: type).toJson(), - // ); - // final Event? event = await getEventById(eventId); - - // if (event == null) { - // debugger(when: kDebugMode); - // throw Exception( - // "null event after creation with eventId $eventId in _createVocabEvent", - // ); - // } - // return event; - // } catch (err, stack) { - // debugger(when: kDebugMode); - // ErrorHandler.logError(e: err, s: stack, data: powerLevels); - // rethrow; - // } - // } - /// Get a list of events in the room that are of type [PangeaEventTypes.construct] /// and have the sender as [userID]. If [count] is provided, the function will /// return at most [count] events. diff --git a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_space_settings_extension.dart similarity index 100% rename from lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart rename to lib/pangea/extensions/pangea_room_extension/room_space_settings_extension.dart diff --git a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_user_permissions_extension.dart similarity index 100% rename from lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart rename to lib/pangea/extensions/pangea_room_extension/room_user_permissions_extension.dart diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index a06d4df7e..5d76a7fc1 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.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/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; @@ -639,34 +638,4 @@ class PangeaMessageEvent { /// Returns a list of [PracticeActivityEvent] for the user's active l2. List get practiceActivities => l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!); - - /// all construct uses for the message, including vocab and grammar - List get allConstructUses => [ - ..._grammarConstructUses, - ..._vocabUses, - ]; - - /// get construct uses of type vocab for the message - List get _vocabUses { - if (originalSent?.tokens != null) { - return originalSent!.content.vocabUses( - event: event, - choreo: originalSent!.choreo, - tokens: originalSent!.tokens!, - ); - } - return []; - } - - /// get construct uses of type grammar for the message - List get _grammarConstructUses => - originalSent?.choreo?.grammarConstructUses(event: event) ?? []; -} - -class URLFinder { - static Iterable getMatches(String text) { - final RegExp exp = - RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'); - return exp.allMatches(text); - } } diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index 191683140..c472752b5 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -18,53 +18,29 @@ class ConstructAnalyticsModel { factory ConstructAnalyticsModel.fromJson(Map json) { final List uses = []; + if (json[_usesKey] is List) { // This is the new format - uses.addAll( - (json[_usesKey] as List) - .map((use) => OneConstructUse.fromJson(use)) - .cast() - .toList(), - ); - } else { - // This is the old format. No data on production should be - // structured this way, but it's useful for testing. - try { - final useValues = (json[_usesKey] as Map).values; - for (final useValue in useValues) { - final lemma = useValue['lemma']; - final lemmaUses = useValue[_usesKey]; - for (final useData in lemmaUses) { - final use = OneConstructUse( - useType: ConstructUseTypeEnum.ga, - lemma: lemma, - form: useData["form"], - constructType: ConstructTypeEnum.grammar, - metadata: ConstructUseMetaData( - eventId: useData["msgId"], - roomId: useData["chatId"], - timeStamp: DateTime.parse(useData["timeStamp"]), - ), - ); - uses.add(use); - } + for (final useJson in json[_usesKey]) { + // grammar construct uses are deprecated so but some are saved + // here we're filtering from data + if (["grammar", "g"].contains(useJson['constructType'])) { + continue; + } else { + uses.add(OneConstructUse.fromJson(useJson)); } - } catch (err, s) { - debugPrint("Error parsing ConstructAnalyticsModel"); - ErrorHandler.logError( - e: err, - s: s, - m: "Error parsing ConstructAnalyticsModel", - ); - debugger(when: kDebugMode); } + } else { + debugger(when: kDebugMode); + ErrorHandler.logError(m: "Analytics room with non-list uses"); } + return ConstructAnalyticsModel( uses: uses, ); } - toJson() { + Map toJson() { return { _usesKey: uses.map((use) => use.toJson()).toList(), }; diff --git a/lib/pangea/models/choreo_record.dart b/lib/pangea/models/choreo_record.dart index fe95dfc09..3586fcee1 100644 --- a/lib/pangea/models/choreo_record.dart +++ b/lib/pangea/models/choreo_record.dart @@ -1,10 +1,6 @@ import 'dart:convert'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; -import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:matrix/matrix.dart'; import 'it_step.dart'; @@ -115,44 +111,6 @@ class ChoreoRecord { String get finalMessage => choreoSteps.isNotEmpty ? choreoSteps.last.text : ""; - - /// Get construct uses of type grammar for the message from this ChoreoRecord. - /// Takes either an event (typically when the Representation itself is - /// available) or construct use metadata (when the event is not available, - /// i.e. immediately after message send) to create the construct uses. - List grammarConstructUses({ - Event? event, - ConstructUseMetaData? metadata, - }) { - final List uses = []; - if (event?.roomId == null && metadata?.roomId == null) { - return uses; - } - metadata ??= ConstructUseMetaData( - roomId: event!.roomId!, - eventId: event.eventId, - timeStamp: event.originServerTs, - ); - - for (final step in choreoSteps) { - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { - final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ?? - step.acceptedOrIgnoredMatch!.match.shortMessage ?? - step.acceptedOrIgnoredMatch!.match.type.typeName.name; - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.ga, - lemma: name, - form: name, - constructType: ConstructTypeEnum.grammar, - id: "${metadata.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", - metadata: metadata, - ), - ); - } - } - return uses; - } } /// A new ChoreoRecordStep is saved in the following cases: diff --git a/lib/pangea/models/representation_content_model.dart b/lib/pangea/models/representation_content_model.dart index 9fb96f1e9..9c6f57c19 100644 --- a/lib/pangea/models/representation_content_model.dart +++ b/lib/pangea/models/representation_content_model.dart @@ -120,7 +120,7 @@ class PangeaRepresentation { tokens.where((token) => token.lemma.saveVocab).toList(); for (final token in tokensToSave) { uses.addAll( - getUsesForToken( + _getUsesForToken( token, metadata, choreo: choreo, @@ -138,7 +138,7 @@ class PangeaRepresentation { /// If the [token] is in the [choreo.acceptedOrIgnoredMatch], it is considered to be a [ConstructUseTypeEnum.ga]. /// If the [token] is in the [choreo.acceptedOrIgnoredMatch.choices], it is considered to be a [ConstructUseTypeEnum.corIt]. /// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa]. - List getUsesForToken( + List _getUsesForToken( PangeaToken token, ConstructUseMetaData metadata, { ChoreoRecord? choreo, diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart deleted file mode 100644 index b859b991b..000000000 --- a/lib/pangea/pages/analytics/construct_list.dart +++ /dev/null @@ -1,572 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; -import 'package:fluffychat/pangea/enum/time_span.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; -import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/utils/string_color.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -class ConstructList extends StatefulWidget { - final ConstructTypeEnum constructType; - final AnalyticsSelected defaultSelected; - final AnalyticsSelected? selected; - final TimeSpan timeSpan; - final PangeaController pangeaController; - final StreamController refreshStream; - - const ConstructList({ - super.key, - required this.constructType, - required this.defaultSelected, - required this.pangeaController, - required this.refreshStream, - required this.timeSpan, - this.selected, - }); - - @override - State createState() => ConstructListState(); -} - -class ConstructListState extends State { - String? langCode; - String? error; - - @override - Widget build(BuildContext context) { - return error != null - ? Center( - child: Text(error!), - ) - : Column( - children: [ - ConstructListView( - pangeaController: widget.pangeaController, - defaultSelected: widget.defaultSelected, - selected: widget.selected, - refreshStream: widget.refreshStream, - timeSpan: widget.timeSpan, - ), - ], - ); - } -} - -// list view of construct events -// parameters -// 1) a list of construct events and -// 2) a boolean indicating whether the list has been initialized -// if not initialized, show loading indicator -// for each tile, -// title = construct.content.lemma -// subtitle = total uses, equal to construct.content.uses.length -// list has a fixed height of 400 and is scrollable -class ConstructListView extends StatefulWidget { - final PangeaController pangeaController; - final AnalyticsSelected defaultSelected; - final TimeSpan timeSpan; - final AnalyticsSelected? selected; - final StreamController refreshStream; - - const ConstructListView({ - super.key, - required this.pangeaController, - required this.defaultSelected, - required this.timeSpan, - required this.refreshStream, - this.selected, - }); - - @override - State createState() => ConstructListViewState(); -} - -class ConstructListViewState extends State { - final ConstructTypeEnum constructType = ConstructTypeEnum.grammar; - final Map _timelinesCache = {}; - final Map _msgEventCache = {}; - final List _msgEvents = []; - bool fetchingConstructs = true; - bool fetchingUses = false; - StreamSubscription? refreshSubscription; - String? currentLemma; - - @override - void initState() { - super.initState(); - widget.pangeaController.analytics - .getConstructs( - constructType: constructType, - forceUpdate: true, - ) - .whenComplete(() => setState(() => fetchingConstructs = false)) - .then( - (value) => setState( - () => constructs = ConstructListModel( - type: constructType, - uses: value, - ), - ), - ); - - refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) { - // postframe callback to let widget rebuild with the new selected parameter - // before sending selected to getConstructs function - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.pangeaController.analytics - .getConstructs( - constructType: constructType, - forceUpdate: true, - ) - .then( - (value) => setState(() { - ConstructListModel( - type: constructType, - uses: value, - ); - }), - ); - }); - }); - } - - @override - void dispose() { - refreshSubscription?.cancel(); - super.dispose(); - } - - void setCurrentLemma(String? lemma) { - currentLemma = lemma; - setState(() {}); - } - - Future getMessageEvent( - OneConstructUse use, - ) async { - final Client client = Matrix.of(context).client; - PangeaMessageEvent msgEvent; - if (_msgEventCache.containsKey(use.msgId)) { - return _msgEventCache[use.msgId]!; - } - final Room? msgRoom = use.getRoom(client); - if (msgRoom == null) { - return null; - } - - Timeline? timeline; - if (_timelinesCache.containsKey(use.chatId)) { - timeline = _timelinesCache[use.chatId]; - } else { - timeline = msgRoom.timeline ?? await msgRoom.getTimeline(); - _timelinesCache[use.chatId] = timeline; - } - - final Event? event = await use.getEvent(client); - if (event == null || timeline == null) { - return null; - } - - msgEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: event.senderId == client.userID, - ); - _msgEventCache[use.msgId] = msgEvent; - return msgEvent; - } - - Future fetchUses() async { - if (fetchingUses) return; - if (currentLemma == null) { - setState(() => _msgEvents.clear()); - return; - } - - setState(() => fetchingUses = true); - try { - final List uses = constructs?.constructList - .firstWhereOrNull( - (element) => element.lemma == currentLemma, - ) - ?.uses ?? - []; - _msgEvents.clear(); - - for (final OneConstructUse use in uses) { - final PangeaMessageEvent? msgEvent = await getMessageEvent(use); - final RepresentationEvent? repEvent = - msgEvent?.originalSent ?? msgEvent?.originalWritten; - if (repEvent?.choreo == null) { - continue; - } - _msgEvents.add(msgEvent!); - } - setState(() => fetchingUses = false); - } catch (err, s) { - setState(() => fetchingUses = false); - debugPrint("Error fetching uses: $err"); - ErrorHandler.logError( - e: err, - s: s, - m: "Failed to fetch uses for current construct $currentLemma", - ); - } - } - - ConstructListModel? constructs; - - // given the current lemma and list of message events, return a list of - // MessageEventMatch objects, which contain one PangeaMessageEvent to one PangeaMatch - // this is because some message events may have has more than one PangeaMatch of a - // given lemma type. - List getMessageEventMatches() { - if (currentLemma == null) return []; - final List allMsgErrorSteps = []; - - for (final msgEvent in _msgEvents) { - if (allMsgErrorSteps.any( - (element) => element.msgEvent.eventId == msgEvent.eventId, - )) { - continue; - } - // get all the pangea matches in that message which have that lemma - final List? msgErrorSteps = msgEvent.errorSteps( - currentLemma!, - ); - if (msgErrorSteps == null) continue; - - allMsgErrorSteps.addAll( - msgErrorSteps.map( - (errorStep) => MessageEventMatch( - msgEvent: msgEvent, - lemmaMatch: errorStep, - ), - ), - ); - } - return allMsgErrorSteps; - } - - Future showConstructMessagesDialog() async { - await showDialog( - context: context, - builder: (c) => ConstructMessagesDialog(controller: this), - ); - } - - @override - Widget build(BuildContext context) { - if (fetchingConstructs || fetchingUses) { - return const Expanded( - child: Center(child: CircularProgressIndicator()), - ); - } - - if (constructs?.constructList.isEmpty ?? true) { - return Expanded( - child: Center(child: Text(L10n.of(context)!.noDataFound)), - ); - } - - return Expanded( - child: ListView.builder( - itemCount: constructs!.constructList.length, - itemBuilder: (context, index) { - return ListTile( - title: Text( - constructs!.constructList[index].lemma, - ), - subtitle: Text( - '${L10n.of(context)!.total} ${constructs!.constructList[index].uses.length}', - ), - onTap: () async { - final String lemma = constructs!.constructList[index].lemma; - setCurrentLemma(lemma); - fetchUses().then((_) => showConstructMessagesDialog()); - }, - ); - }, - ), - ); - } -} - -class ConstructMessagesDialog extends StatelessWidget { - final ConstructListViewState controller; - const ConstructMessagesDialog({ - super.key, - required this.controller, - }); - - @override - Widget build(BuildContext context) { - if (controller.currentLemma == null || controller.constructs == null) { - return const AlertDialog(content: CircularProgressIndicator.adaptive()); - } - - final msgEventMatches = controller.getMessageEventMatches(); - - final currentConstruct = - controller.constructs!.constructList.firstWhereOrNull( - (construct) => construct.lemma == controller.currentLemma, - ); - final noData = currentConstruct == null || - currentConstruct.uses.length > controller._msgEvents.length; - - return AlertDialog( - title: Center(child: Text(controller.currentLemma!)), - content: SizedBox( - height: noData ? 90 : 250, - width: noData ? 200 : 400, - child: Column( - children: [ - if (noData) - Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(L10n.of(context)!.roomDataMissing), - ), - ), - Expanded( - child: ListView( - children: [ - ...msgEventMatches.mapIndexed( - (index, event) => Column( - children: [ - ConstructMessage( - msgEvent: event.msgEvent, - lemma: controller.currentLemma!, - errorMessage: event.lemmaMatch, - ), - if (index < msgEventMatches.length - 1) - const Divider(height: 1), - ], - ), - ), - ], - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context, rootNavigator: false).pop(), - child: Text( - L10n.of(context)!.close.toUpperCase(), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ); - } -} - -class ConstructMessage extends StatelessWidget { - final PangeaMessageEvent msgEvent; - final PangeaMatch errorMessage; - final String lemma; - - const ConstructMessage({ - super.key, - required this.msgEvent, - required this.errorMessage, - required this.lemma, - }); - - @override - Widget build(BuildContext context) { - final String? chosen = errorMessage.match.choices - ?.firstWhereOrNull( - (element) => element.selected == true, - ) - ?.value; - - if (chosen == null) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - ConstructMessageMetadata(msgEvent: msgEvent), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FutureBuilder( - future: msgEvent.event.fetchSenderUser(), - builder: (context, snapshot) { - final displayname = snapshot.data?.calcDisplayname() ?? - msgEvent.event.senderFromMemoryOrFallback - .calcDisplayname(); - return Text( - displayname, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: (Theme.of(context).brightness == - Brightness.light - ? displayname.color - : displayname.lightColorText), - ), - ); - }, - ), - ConstructMessageBubble( - errorText: errorMessage.match.fullText, - replacementText: chosen, - start: errorMessage.match.offset, - end: - errorMessage.match.offset + errorMessage.match.length, - ), - ], - ), - ], - ), - ), - ], - ), - ); - } -} - -class ConstructMessageBubble extends StatelessWidget { - final String errorText; - final String replacementText; - final int start; - final int end; - - const ConstructMessageBubble({ - super.key, - required this.errorText, - required this.replacementText, - required this.start, - required this.end, - }); - - @override - Widget build(BuildContext context) { - final defaultStyle = TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor, - height: 1.3, - ); - - return IntrinsicWidth( - child: Material( - color: Theme.of(context).colorScheme.primaryContainer, - clipBehavior: Clip.antiAlias, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4), - topRight: Radius.circular(AppConfig.borderRadius), - bottomLeft: Radius.circular(AppConfig.borderRadius), - bottomRight: Radius.circular(AppConfig.borderRadius), - ), - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: errorText.substring(0, start), - style: defaultStyle, - ), - TextSpan( - text: errorText.substring(start, end), - style: defaultStyle.merge( - TextStyle( - backgroundColor: Colors.red.withOpacity(0.25), - decoration: TextDecoration.lineThrough, - decorationThickness: 2.5, - ), - ), - ), - const TextSpan(text: " "), - TextSpan( - text: replacementText, - style: defaultStyle.merge( - TextStyle( - backgroundColor: Colors.green.withOpacity(0.25), - ), - ), - ), - TextSpan( - text: errorText.substring(end), - style: defaultStyle, - ), - ], - ), - ), - ), - ), - ); - } -} - -class ConstructMessageMetadata extends StatelessWidget { - final PangeaMessageEvent msgEvent; - - const ConstructMessageMetadata({ - super.key, - required this.msgEvent, - }); - - @override - Widget build(BuildContext context) { - final String roomName = msgEvent.event.room.getLocalizedDisplayname(); - return Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 30, 0), - child: Column( - children: [ - Text( - msgEvent.event.originServerTs.localizedTime(context), - style: TextStyle(fontSize: 13 * AppConfig.fontSizeFactor), - ), - Text(roomName), - ], - ), - ); - } -} - -class MessageEventMatch { - final PangeaMessageEvent msgEvent; - final PangeaMatch lemmaMatch; - - MessageEventMatch({ - required this.msgEvent, - required this.lemmaMatch, - }); -} diff --git a/lib/pangea/pages/analytics/list_summary_analytics.dart b/lib/pangea/pages/analytics/list_summary_analytics.dart deleted file mode 100644 index 02bb5d3fb..000000000 --- a/lib/pangea/pages/analytics/list_summary_analytics.dart +++ /dev/null @@ -1,101 +0,0 @@ -// import 'dart:math'; - -// import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter_gen/gen_l10n/l10n.dart'; - -// import '../../enum/use_type.dart'; - -// class ListSummaryAnalytics extends StatelessWidget { -// final ChartAnalyticsModel? chartAnalytics; - -// const ListSummaryAnalytics({super.key, this.chartAnalytics}); - -// TimeSeriesTotals? get totals => chartAnalytics?.totals; - -// String spacer(int baseLength, int number) => -// " " * max(baseLength - number.toString().length, 0); - -// WidgetSpan spacerIconText( -// String toolTip, -// String space, -// IconData icon, -// int value, -// Color? color, [ -// percentage = true, -// ]) => -// WidgetSpan( -// child: Tooltip( -// message: toolTip, -// child: RichText( -// text: TextSpan( -// children: [ -// TextSpan( -// text: space, -// ), -// WidgetSpan(child: Icon(icon, size: 14, color: color)), -// TextSpan( -// text: " $value${percentage ? "%" : ""}", -// style: TextStyle(color: color), -// ), -// ], -// ), -// ), -// ), -// ); - -// @override -// Widget build(BuildContext context) { -// if (totals == null) { -// return const LinearProgressIndicator(); -// } -// final l10n = L10n.of(context); - -// return RichText( -// text: TextSpan( -// children: [ -// spacerIconText( -// L10n.of(context) != null -// ? L10n.of(context)!.totalMessages -// : "Total messages sent", -// "", -// Icons.chat_bubble, -// totals!.all, -// Theme.of(context).textTheme.bodyLarge!.color, -// false, -// ), -// if (totals!.all != 0) ...[ -// spacerIconText( -// l10n != null ? l10n.taTooltip : "With translation assistance", -// spacer(8, totals!.all), -// UseType.ta.iconData, -// totals!.taPercent, -// UseType.ta.color(context), -// ), -// spacerIconText( -// l10n != null ? l10n.gaTooltip : "With grammar assistance", -// spacer(4, totals!.taPercent), -// UseType.ga.iconData, -// totals!.gaPercent, -// UseType.ga.color(context), -// ), -// spacerIconText( -// l10n != null ? l10n.waTooltip : "Without assistance", -// spacer(4, totals!.gaPercent), -// UseType.wa.iconData, -// totals!.waPercent, -// UseType.wa.color(context), -// ), -// spacerIconText( -// l10n != null ? l10n.unTooltip : "Other", -// spacer(4, totals!.waPercent), -// UseType.un.iconData, -// totals!.unPercent, -// UseType.un.color(context), -// ), -// ], -// ], -// ), -// ); -// } -// } diff --git a/lib/pangea/utils/logout.dart b/lib/pangea/utils/logout.dart index def632828..f9f816893 100644 --- a/lib/pangea/utils/logout.dart +++ b/lib/pangea/utils/logout.dart @@ -20,7 +20,7 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async { final matrix = Matrix.of(context); // before wiping out locally cached construct data, save it to the server - await MatrixState.pangeaController.myAnalytics + await MatrixState.pangeaController.putAnalytics .sendLocalAnalyticsToAnalyticsRoom(onLogout: true); await showFutureLoadingDialog( diff --git a/lib/pangea/widgets/animations/gain_points.dart b/lib/pangea/widgets/animations/gain_points.dart index d9d9a0111..c41b048bf 100644 --- a/lib/pangea/widgets/animations/gain_points.dart +++ b/lib/pangea/widgets/animations/gain_points.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -29,8 +29,8 @@ class PointsGainedAnimationState extends State late Animation _fadeAnimation; StreamSubscription? _pointsSubscription; - int? get _prevXP => MatrixState.pangeaController.analytics.prevXP; - int? get _currentXP => MatrixState.pangeaController.analytics.currentXP; + int? get _prevXP => MatrixState.pangeaController.getAnalytics.prevXP; + int? get _currentXP => MatrixState.pangeaController.getAnalytics.currentXP; int? _addedPoints; @override @@ -62,7 +62,7 @@ class PointsGainedAnimationState extends State ); _pointsSubscription = MatrixState - .pangeaController.analytics.analyticsStream.stream + .pangeaController.getAnalytics.analyticsStream.stream .listen(_showPointsGained); } diff --git a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart index 980ee1488..a9798d1ca 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart @@ -54,21 +54,21 @@ class LearningProgressIndicatorsState // int get totalXP => _pangeaController.analytics.currentXP; // int get level => _pangeaController.analytics.level; List currentConstructs = []; - int get currentXP => _pangeaController.analytics.calcXP(currentConstructs); - int get localXP => _pangeaController.analytics.calcXP( - _pangeaController.analytics.locallyCachedConstructs, + int get currentXP => _pangeaController.getAnalytics.calcXP(currentConstructs); + int get localXP => _pangeaController.getAnalytics.calcXP( + _pangeaController.getAnalytics.locallyCachedConstructs, ); int get serverXP => currentXP - localXP; - int get level => _pangeaController.analytics.level; + int get level => _pangeaController.getAnalytics.level; @override void initState() { super.initState(); updateAnalyticsData( - _pangeaController.analytics.analyticsStream.value?.constructs ?? [], + _pangeaController.getAnalytics.analyticsStream.value?.constructs ?? [], ); _analyticsUpdateSubscription = _pangeaController - .analytics.analyticsStream.stream + .getAnalytics.analyticsStream.stream .listen((update) => updateAnalyticsData(update.constructs)); } @@ -146,12 +146,12 @@ class LearningProgressIndicatorsState ? const Color.fromARGB(255, 0, 190, 83) : Theme.of(context).colorScheme.primary, currentPoints: currentXP, - widthMultiplier: _pangeaController.analytics.levelProgress, + widthMultiplier: _pangeaController.getAnalytics.levelProgress, ), LevelBarDetails( fillColor: Theme.of(context).colorScheme.primary, currentPoints: serverXP, - widthMultiplier: _pangeaController.analytics.serverLevelProgress, + widthMultiplier: _pangeaController.getAnalytics.serverLevelProgress, ), ], ); diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index c40062738..9454a2517 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -1,7 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/span_data_type.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; @@ -125,7 +125,7 @@ class SpanCardState extends State { selectedChoiceIndex = index; if (selectedChoice != null) { if (!selectedChoice!.selected) { - MatrixState.pangeaController.myAnalytics.addDraftUses( + MatrixState.pangeaController.putAnalytics.addDraftUses( selectedChoice!.tokens, widget.roomId, selectedChoice!.isBestCorrection @@ -158,7 +158,7 @@ class SpanCardState extends State { /// Adds the ignored tokens to locally cached analytics void addIgnoredTokenUses() { - MatrixState.pangeaController.myAnalytics.addDraftUses( + MatrixState.pangeaController.putAnalytics.addDraftUses( ignoredTokens ?? [], widget.roomId, ConstructUseTypeEnum.ignIGC, diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index d37084283..2eb4c3523 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.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'; @@ -73,7 +73,7 @@ class MultipleChoiceActivityState extends State { return; } - MatrixState.pangeaController.myAnalytics.setState( + MatrixState.pangeaController.putAnalytics.setState( AnalyticsStream( // note - this maybe should be the activity event id eventId: diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 00b11f658..64de20714 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.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_activity_event.dart'; diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart index 69be7f6c2..65aec4702 100644 --- a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -26,7 +26,7 @@ class TargetTokensController { _targetTokens = await _initialize(pangeaMessageEvent); final allConstructs = MatrixState - .pangeaController.analytics.analyticsStream.value?.constructs; + .pangeaController.getAnalytics.analyticsStream.value?.constructs; await updateTokensWithConstructs( allConstructs ?? [], pangeaMessageEvent, diff --git a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart index 810074a76..168758137 100644 --- a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart +++ b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart @@ -1,7 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/put_analytics_controller.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/practice_activities.dart/practice_activity_record_model.dart'; @@ -69,7 +69,7 @@ class WordFocusListeningActivityState return; } - MatrixState.pangeaController.myAnalytics.setState( + MatrixState.pangeaController.putAnalytics.setState( AnalyticsStream( // note - this maybe should be the activity event id eventId: diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart index 80dd7fa9d..b0276e11c 100644 --- a/lib/pangea/widgets/user_settings/p_language_dialog.dart +++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart @@ -93,7 +93,12 @@ Future pLanguageDialog( context: context, future: () async { try { - pangeaController.myAnalytics + //@ggurdin while this is obviously working, it feels pretty hidden + //and could lead to errors if someone where to change the user L2 via some + // other means. with analytics being dependent on languages, it probably + // would make sense for analytics to listen to the language stateStream + // and update in this case + pangeaController.putAnalytics .sendLocalAnalyticsToAnalyticsRoom() .then((_) { pangeaController.userController.updateProfile( @@ -109,11 +114,11 @@ Future pLanguageDialog( }).then((_) { // if the profile update is successful, reset cached analytics // data, since analytics data corresponds to the user's L2 - pangeaController.myAnalytics.dispose(); - pangeaController.analytics.dispose(); + pangeaController.putAnalytics.dispose(); + pangeaController.getAnalytics.dispose(); - pangeaController.myAnalytics.initialize(); - pangeaController.analytics.initialize(); + pangeaController.putAnalytics.initialize(); + pangeaController.getAnalytics.initialize(); Navigator.pop(context); }); From fd71a736adcdfab1961e3053649c8f7d4c652bf9 Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:29:19 -0500 Subject: [PATCH 19/23] Don't change .env in main.dart --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 847b012b2..9f5e656bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,7 +23,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env.local_choreo"); + await dotenv.load(fileName: ".env"); } catch (e) { Logs().e('Failed to load .env file', e); } From 8ee4ea31c83eef93e788534e033723b6d839ceef Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 7 Nov 2024 10:03:34 -0500 Subject: [PATCH 20/23] addressed some of Will's questions --- lib/pages/chat_list/chat_list.dart | 5 +-- .../controllers/get_analytics_controller.dart | 4 +- lib/pangea/controllers/pangea_controller.dart | 7 ++++ .../controllers/put_analytics_controller.dart | 29 +++++++++++-- lib/pangea/controllers/user_controller.dart | 6 +++ .../user_settings/p_language_dialog.dart | 41 ++++++------------- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 770d9204f..9ad418e36 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -1016,9 +1016,6 @@ class ChatListController extends State } // #Pangea - //@ggurdin why is are these two initialized separately? why not in the _initPangeaControllers? - MatrixState.pangeaController.putAnalytics.initialize(); - MatrixState.pangeaController.getAnalytics.initialize(); await _initPangeaControllers(client); // Pangea# if (!mounted) return; @@ -1029,6 +1026,8 @@ class ChatListController extends State // #Pangea Future _initPangeaControllers(Client client) async { + MatrixState.pangeaController.putAnalytics.initialize(); + MatrixState.pangeaController.getAnalytics.initialize(); if (mounted) { final PangeaController pangeaController = MatrixState.pangeaController; GoogleAnalytics.analyticsUserUpdate(client.userID); diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index dba450091..181279f3d 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -128,11 +128,11 @@ class GetAnalyticsController { type: ConstructTypeEnum.vocab, ); - final errors = ConstructListModel( + final morphs = ConstructListModel( uses: constructs, type: ConstructTypeEnum.morph, ); - return words.points + errors.points; + return words.points + morphs.points; } List get allConstructUses { diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index e0b33b054..3e737113d 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -169,6 +169,13 @@ class PangeaController { GoogleAnalytics.analyticsUserUpdate(matrixState.client.userID); } + Future resetAnalytics() async { + putAnalytics.dispose(); + getAnalytics.dispose(); + putAnalytics.initialize(); + getAnalytics.initialize(); + } + void startChatWithBotIfNotPresent() { Future.delayed(const Duration(milliseconds: 10000), () async { // check if user is logged in diff --git a/lib/pangea/controllers/put_analytics_controller.dart b/lib/pangea/controllers/put_analytics_controller.dart index 37768cdf0..f887a6d66 100644 --- a/lib/pangea/controllers/put_analytics_controller.dart +++ b/lib/pangea/controllers/put_analytics_controller.dart @@ -24,6 +24,7 @@ class PutAnalyticsController extends BaseController { CachedStreamController analyticsUpdateStream = CachedStreamController(); StreamSubscription? _analyticsStream; + StreamSubscription? _languageStream; Timer? _updateTimer; Client get _client => _pangeaController.matrixState.client; @@ -57,6 +58,15 @@ class PutAnalyticsController extends BaseController { _analyticsStream ??= stateStream.listen((data) => _onNewAnalyticsData(data)); + // Listen for changes to the user's language settings + _languageStream ??= + _pangeaController.userController.stateStream.listen((update) { + if (update is Map) { + final previousL2 = update['prev_target_lang']; + _onUpdateLanguages(previousL2); + } + }); + _refreshAnalyticsIfOutdated(); } @@ -68,6 +78,8 @@ class PutAnalyticsController extends BaseController { lastUpdatedCompleter = Completer(); _analyticsStream?.cancel(); _analyticsStream = null; + _languageStream?.cancel(); + _languageStream = null; _refreshAnalyticsIfOutdated(); clearMessagesSinceUpdate(); } @@ -131,6 +143,13 @@ class PutAnalyticsController extends BaseController { ); } + Future _onUpdateLanguages(String previousL2) async { + await sendLocalAnalyticsToAnalyticsRoom( + l2Override: previousL2, + ); + _pangeaController.resetAnalytics(); + } + void addDraftUses( List tokens, String roomID, @@ -299,6 +318,7 @@ class PutAnalyticsController extends BaseController { /// since the last update and notifies the [analyticsUpdateStream]. Future sendLocalAnalyticsToAnalyticsRoom({ onLogout = false, + String? l2Override, }) async { if (_pangeaController.matrixState.client.userID == null) return; if (!(_updateCompleter?.isCompleted ?? true)) { @@ -307,7 +327,7 @@ class PutAnalyticsController extends BaseController { } _updateCompleter = Completer(); try { - await _updateAnalytics(); + await _updateAnalytics(l2Override: l2Override); clearMessagesSinceUpdate(); lastUpdated = DateTime.now(); @@ -331,7 +351,7 @@ class PutAnalyticsController extends BaseController { /// Updates the analytics by sending cached analytics data to the analytics room. /// The analytics room is determined based on the user's current target language. - Future _updateAnalytics() async { + Future _updateAnalytics({String? l2Override}) async { // if there's no cached construct data, there's nothing to send final cachedConstructs = _pangeaController.getAnalytics.messagesSinceUpdate; final bool onlyDraft = cachedConstructs.length == 1 && @@ -339,10 +359,11 @@ class PutAnalyticsController extends BaseController { if (cachedConstructs.isEmpty || onlyDraft) return; // if missing important info, don't send analytics. Could happen if user just signed up. - if (userL2 == null || _client.userID == null) return; + final l2Code = l2Override ?? userL2; + if (l2Code == null || _client.userID == null) return; // analytics room for the user and current target language - final Room? analyticsRoom = await _client.getMyAnalyticsRoom(userL2!); + final Room? analyticsRoom = await _client.getMyAnalyticsRoom(l2Code); // and send cached analytics data to the room await analyticsRoom?.sendConstructsEvent( diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index 53893cb7b..c076063c3 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -73,8 +73,14 @@ class UserController extends BaseController { Profile Function(Profile) update, { waitForDataInSync = false, }) async { + final prevTargetLang = profile.userSettings.targetLanguage; + final Profile updatedProfile = update(profile); await updatedProfile.saveProfileData(waitForDataInSync: waitForDataInSync); + + if (prevTargetLang != updatedProfile.userSettings.targetLanguage) { + setState({'prev_target_lang': prevTargetLang}); + } } /// Creates a new profile for the user with the given date of birth. diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart index b0276e11c..5b7c7b73d 100644 --- a/lib/pangea/widgets/user_settings/p_language_dialog.dart +++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart @@ -93,35 +93,18 @@ Future pLanguageDialog( context: context, future: () async { try { - //@ggurdin while this is obviously working, it feels pretty hidden - //and could lead to errors if someone where to change the user L2 via some - // other means. with analytics being dependent on languages, it probably - // would make sense for analytics to listen to the language stateStream - // and update in this case - pangeaController.putAnalytics - .sendLocalAnalyticsToAnalyticsRoom() - .then((_) { - pangeaController.userController.updateProfile( - (profile) { - profile.userSettings.sourceLanguage = - selectedSourceLanguage.langCode; - profile.userSettings.targetLanguage = - selectedTargetLanguage.langCode; - return profile; - }, - waitForDataInSync: true, - ); - }).then((_) { - // if the profile update is successful, reset cached analytics - // data, since analytics data corresponds to the user's L2 - pangeaController.putAnalytics.dispose(); - pangeaController.getAnalytics.dispose(); - - pangeaController.putAnalytics.initialize(); - pangeaController.getAnalytics.initialize(); - - Navigator.pop(context); - }); + await pangeaController.userController + .updateProfile( + (profile) { + profile.userSettings.sourceLanguage = + selectedSourceLanguage.langCode; + profile.userSettings.targetLanguage = + selectedTargetLanguage.langCode; + return profile; + }, + waitForDataInSync: true, + ); + Navigator.pop(context); } catch (err, s) { debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: s); From 1f5d66e203e684286cff5e8975935b113f4ecde3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 7 Nov 2024 10:05:49 -0500 Subject: [PATCH 21/23] deleted irrelevant constructs filtering function --- .../controllers/get_analytics_controller.dart | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index 181279f3d..a22d826cd 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; -import 'package:fluffychat/pangea/constants/match_rule_ids.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; @@ -227,25 +226,20 @@ class GetAnalyticsController { final List constructEvents = await allMyConstructs(); - final List unfilteredUses = []; + final List uses = []; for (final event in constructEvents) { - unfilteredUses.addAll(event.content.uses); + uses.addAll(event.content.uses); } - // filter out any constructs that are not relevant to the user - final List filteredUses = await filterConstructs( - unfilteredConstructs: unfilteredUses, - ); - // if there isn't already a valid, local cache, cache the filtered uses if (local == null) { cacheConstructs( constructType: constructType, - uses: filteredUses, + uses: uses, ); } - return filteredUses; + return uses; } /// Get the last time the user updated their analytics for their current l2 @@ -272,24 +266,6 @@ class GetAnalyticsController { return await analyticsRoom.getAnalyticsEvents(userId: client.userID!) ?? []; } - /// Filter out constructs that are not relevant to the user, specifically those from - /// rooms in which the user is a teacher and those that are interative translation span constructs - /// @ggurdin - is this still relevant now that we're not doing grammar constructs? - /// maybe it should actually be filtering all grammar uses, though this is maybe more efficiently done - /// in the fromJson of the model reading the event content, then maybe we can get rid of that enum entry entirely - Future> filterConstructs({ - required List unfilteredConstructs, - }) async { - return unfilteredConstructs - .where( - (use) => - use.lemma != "Try interactive translation" && - use.lemma != "itStart" || - use.lemma != MatchRuleIds.interactiveTranslation, - ) - .toList(); - } - /// Get the cached construct uses for the current user, if it exists List? getConstructsLocal({ ConstructTypeEnum? constructType, From 4a2a8bf7bda1b79e1d3856d12c296a364e6b6f94 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 7 Nov 2024 12:47:43 -0500 Subject: [PATCH 22/23] convert choreo record into morph GA uses on send --- lib/pages/chat/chat.dart | 17 +++++-- lib/pangea/enum/construct_use_type_enum.dart | 4 +- lib/pangea/models/pangea_match_model.dart | 2 +- .../models/representation_content_model.dart | 51 +++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 071c5f10c..2d47def1b 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -683,11 +683,18 @@ class ChatController extends State AnalyticsStream( eventId: msgEventId, roomId: room.id, - constructs: originalSent!.vocabUses( - choreo: choreo, - tokens: tokensSent!.tokens, - metadata: metadata, - ), + constructs: [ + ...originalSent!.vocabUses( + choreo: choreo, + tokens: tokensSent!.tokens, + metadata: metadata, + ), + ...originalSent.morphConstructUses( + choreo: choreo, + tokens: tokensSent.tokens, + metadata: metadata, + ), + ], origin: AnalyticsUpdateOrigin.sendMessage, ), ); diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart index 196cf89b4..cb6b2a257 100644 --- a/lib/pangea/enum/construct_use_type_enum.dart +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -100,7 +100,6 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corWL: return 3; - case ConstructUseTypeEnum.ga: case ConstructUseTypeEnum.corIGC: return 2; @@ -115,6 +114,9 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.nan: return 0; + case ConstructUseTypeEnum.ga: + return -1; + case ConstructUseTypeEnum.incIt: case ConstructUseTypeEnum.incIGC: return -2; diff --git a/lib/pangea/models/pangea_match_model.dart b/lib/pangea/models/pangea_match_model.dart index 827816890..b27221e1a 100644 --- a/lib/pangea/models/pangea_match_model.dart +++ b/lib/pangea/models/pangea_match_model.dart @@ -108,7 +108,7 @@ class PangeaMatch { } bool isOffsetInMatchSpan(int offset) => - offset >= match.offset && offset <= match.offset + match.length; + offset >= match.offset && offset < match.offset + match.length; Color get underlineColor { switch (match.rule?.id ?? "unknown") { diff --git a/lib/pangea/models/representation_content_model.dart b/lib/pangea/models/representation_content_model.dart index 9c6f57c19..2d6bbb714 100644 --- a/lib/pangea/models/representation_content_model.dart +++ b/lib/pangea/models/representation_content_model.dart @@ -131,6 +131,57 @@ class PangeaRepresentation { return uses; } + List morphConstructUses({ + required List tokens, + Event? event, + ConstructUseMetaData? metadata, + ChoreoRecord? choreo, + }) { + final List uses = []; + + if (event?.roomId == null && metadata?.roomId == null) { + return uses; + } + + if (choreo == null) { + return uses; + } + + metadata ??= ConstructUseMetaData( + roomId: event!.roomId!, + eventId: event.eventId, + timeStamp: event.originServerTs, + ); + + for (final step in choreo.choreoSteps) { + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { + final List tokensForMatch = []; + for (final token in tokens) { + if (step.acceptedOrIgnoredMatch!.isOffsetInMatchSpan( + token.text.offset, + )) { + tokensForMatch.add(token); + } + } + + for (final token in tokensForMatch) { + token.morph.forEach((key, value) { + uses.add( + OneConstructUse( + useType: ConstructUseTypeEnum.ga, + lemma: value, + categories: [key], + constructType: ConstructTypeEnum.morph, + metadata: metadata!, + ), + ); + }); + } + } + } + return uses; + } + /// Returns a [OneConstructUse] for the given [token] /// If there is no [choreo], the [token] is /// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language. From dd5d3f59eedd91c5985442c09022adcf6f0244c7 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 7 Nov 2024 15:45:45 -0500 Subject: [PATCH 23/23] update vocabUses function to save tokens in matches as GA construct uses --- lib/pages/chat/chat.dart | 7 +- .../models/representation_content_model.dart | 107 ++++++------------ 2 files changed, 36 insertions(+), 78 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 2d47def1b..10395de9e 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -684,16 +684,11 @@ class ChatController extends State eventId: msgEventId, roomId: room.id, constructs: [ - ...originalSent!.vocabUses( + ...originalSent!.vocabAndMorphUses( choreo: choreo, tokens: tokensSent!.tokens, metadata: metadata, ), - ...originalSent.morphConstructUses( - choreo: choreo, - tokens: tokensSent.tokens, - metadata: metadata, - ), ], origin: AnalyticsUpdateOrigin.sendMessage, ), diff --git a/lib/pangea/models/representation_content_model.dart b/lib/pangea/models/representation_content_model.dart index 2d6bbb714..c5559ba54 100644 --- a/lib/pangea/models/representation_content_model.dart +++ b/lib/pangea/models/representation_content_model.dart @@ -89,13 +89,13 @@ class PangeaRepresentation { return data; } - /// Get construct uses of type vocab for the message. + /// Get construct uses for the message that weren't captured during language assistance. /// Takes a list of tokens and a choreo record, which is searched /// through for each token for its construct use type. /// Also takes either an event (typically when the Representation itself is /// available) or construct use metadata (when the event is not available, /// i.e. immediately after message send) to create the construct use. - List vocabUses({ + List vocabAndMorphUses({ required List tokens, Event? event, ConstructUseMetaData? metadata, @@ -131,64 +131,15 @@ class PangeaRepresentation { return uses; } - List morphConstructUses({ - required List tokens, - Event? event, - ConstructUseMetaData? metadata, - ChoreoRecord? choreo, - }) { - final List uses = []; - - if (event?.roomId == null && metadata?.roomId == null) { - return uses; - } - - if (choreo == null) { - return uses; - } - - metadata ??= ConstructUseMetaData( - roomId: event!.roomId!, - eventId: event.eventId, - timeStamp: event.originServerTs, - ); - - for (final step in choreo.choreoSteps) { - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { - final List tokensForMatch = []; - for (final token in tokens) { - if (step.acceptedOrIgnoredMatch!.isOffsetInMatchSpan( - token.text.offset, - )) { - tokensForMatch.add(token); - } - } - - for (final token in tokensForMatch) { - token.morph.forEach((key, value) { - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.ga, - lemma: value, - categories: [key], - constructType: ConstructTypeEnum.morph, - metadata: metadata!, - ), - ); - }); - } - } - } - return uses; - } - /// Returns a [OneConstructUse] for the given [token] /// If there is no [choreo], the [token] is /// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language. /// Later on, we may want to consider putting it in some category of like 'pending' - /// If the [token] is in the [choreo.acceptedOrIgnoredMatch], it is considered to be a [ConstructUseTypeEnum.ga]. - /// If the [token] is in the [choreo.acceptedOrIgnoredMatch.choices], it is considered to be a [ConstructUseTypeEnum.corIt]. - /// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa]. + /// + /// For both vocab and morph constructs, we should + /// 1) give wa if no assistance was used + /// 2) give ga if IGC was used and + /// 3) make no use if IT was used List _getUsesForToken( PangeaToken token, ConstructUseMetaData metadata, { @@ -218,7 +169,7 @@ class PangeaRepresentation { if (lemma.saveVocab) { uses.add( lemma.toVocabUse( - inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, + useType, metadata, ), ); @@ -231,8 +182,12 @@ class PangeaRepresentation { /// is in the overall step text, then token was a ga final bool isAcceptedMatch = step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted; - final bool isITStep = step.itStep != null; - if (!isAcceptedMatch && !isITStep) continue; + + // if the token was in an IT match, return no uses + if (step.itStep != null) return []; + + // if this step was not accepted, continue + if (!isAcceptedMatch) continue; if (isAcceptedMatch && step.acceptedOrIgnoredMatch?.match.choices != null) { @@ -240,25 +195,33 @@ class PangeaRepresentation { final bool stepContainedToken = choices.any( (choice) => // if this choice contains the token's content - choice.value.contains(content) && - // if the complete input text after this step - // contains the choice (why is this here?) - step.text.contains(choice.value), + choice.value.contains(content), ); if (stepContainedToken) { - return []; - } - } - - if (isITStep && step.itStep?.chosenContinuance != null) { - final bool pickedThroughIT = - step.itStep!.chosenContinuance!.text.contains(content); - if (pickedThroughIT) { - return []; + // give ga if IGC was used + uses.add( + lemma.toVocabUse( + ConstructUseTypeEnum.ga, + metadata, + ), + ); + for (final entry in token.morph.entries) { + uses.add( + OneConstructUse( + useType: ConstructUseTypeEnum.ga, + lemma: entry.value, + categories: [entry.key], + constructType: ConstructTypeEnum.morph, + metadata: metadata, + ), + ); + } + return uses; } } } + // the token wasn't found in any IT or IGC step, so it was wa for (final entry in token.morph.entries) { uses.add( OneConstructUse(