From d468b50785186a992a01984ff8586a86a76bc481 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Wed, 6 Nov 2024 17:21:34 +0700 Subject: [PATCH 01/16] fix accept replacement incorrectly reconstructing new full text --- lib/pangea/models/igc_text_data_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 60497955b..7ef2afa3b 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -141,8 +141,9 @@ class IGCTextData { // start is inclusive final startIndex = tokenIndexByOffset(pangeaMatch.match.offset); // end is exclusive, hence the +1 + // use pangeaMatch.matchContent.trim().length instead of pangeaMatch.match.length since pangeaMatch.match.length may include leading/trailing spaces final endIndex = tokenIndexByOffset( - pangeaMatch.match.offset + pangeaMatch.match.length, + pangeaMatch.match.offset + pangeaMatch.matchContent.trim().length, ) + 1; @@ -159,7 +160,6 @@ class IGCTextData { newTokens.replaceRange(startIndex, endIndex, replacement.tokens); final String newFullText = PangeaToken.reconstructText(newTokens); - if (newFullText != originalInput && kDebugMode) { PangeaToken.reconstructText(newTokens, debugWalkThrough: true); ErrorHandler.logError( From e48d8d57c9d7c33997dd74a8fa33b5f428790bc0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 6 Nov 2024 09:30:11 -0500 Subject: [PATCH 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 14/16] 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 15/16] 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 16/16] 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(