From bf29b283643a8a2018be80557c493e599ba693d2 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:04:33 -0400 Subject: [PATCH] feat: underline new tokens and animate collecting them on click --- lib/l10n/intl_en.arb | 1 + lib/pages/chat/events/html_message.dart | 27 ++-- .../construct_use_type_enum.dart | 14 +- .../toolbar/utils/token_rendering_util.dart | 9 +- .../widgets/message_selection_overlay.dart | 67 +++++++++- .../widgets/reading_assistance_content.dart | 8 +- .../widgets/word_zoom/new_word_overlay.dart | 121 ++++++++++++++++++ .../widgets/word_zoom/word_zoom_widget.dart | 18 ++- 8 files changed, 237 insertions(+), 28 deletions(-) create mode 100644 lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0f6b2899e..33b2205ea 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4589,6 +4589,7 @@ "constructUseIncMDesc": "Incorrect in grammar activity", "constructUseIgnMDesc": "Ignored in grammar activity", "constructUseEmojiDesc": "Correct in emoji activity", + "constructUseCollected": "Collected in chat", "constructUseNanDesc": "Not applicable", "xpIntoLevel": "{currentXP} / {maxXP} XP", "@xpIntoLevel": { diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 877ddf428..4fbe27a49 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,14 +1,4 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_highlighter/flutter_highlighter.dart'; -import 'package:flutter_highlighter/themes/shades-of-purple.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:html/dom.dart' as dom; -import 'package:html/parser.dart' as parser; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/common/utils/any_state_holder.dart'; @@ -24,6 +14,15 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_highlighter/flutter_highlighter.dart'; +import 'package:flutter_highlighter/themes/shades-of-purple.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:html/dom.dart' as dom; +import 'package:html/parser.dart' as parser; +import 'package:matrix/matrix.dart'; + import '../../../utils/url_launcher.dart'; class HtmlMessage extends StatelessWidget { @@ -324,6 +323,10 @@ class HtmlMessage extends StatelessWidget { ? isSelected!.call(token) : false; + final isNew = token != null && + overlayController != null && + overlayController!.isNewToken(token); + final tokenWidth = renderer.tokenTextWidthForContainer( context, node.text, @@ -352,6 +355,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, selected, + isNew, ), ), width: tokenWidth, @@ -386,6 +390,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, selected, + isNew, ), ), linkStyle: linkStyle, @@ -528,6 +533,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, false, + false, ), ), ), @@ -543,6 +549,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, false, + false, ), ), // Pangea# diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index ef29da935..7cda1e003 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_downloads/space_analytics_summary_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; +import 'package:flutter/material.dart'; enum ConstructUseTypeEnum { /// produced in chat by user, igc was run, and we've judged it to be a correct use @@ -66,6 +65,9 @@ enum ConstructUseTypeEnum { incMM, ignMM, + /// lemma collected by clicking on it + click, + /// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client nan } @@ -135,6 +137,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return L10n.of(context).constructUseIncMmDesc; case ConstructUseTypeEnum.ignMM: return L10n.of(context).constructUseIgnMmDesc; + case ConstructUseTypeEnum.click: + return L10n.of(context).constructUseCollected; case ConstructUseTypeEnum.nan: return L10n.of(context).constructUseNanDesc; } @@ -185,6 +189,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.nan: return Icons.help; + case ConstructUseTypeEnum.click: + return Icons.format_color_text; } } @@ -211,6 +217,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corIGC: case ConstructUseTypeEnum.corL: + case ConstructUseTypeEnum.click: return 2; case ConstructUseTypeEnum.corIt: @@ -279,6 +286,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incMM: case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.em: + case ConstructUseTypeEnum.click: case ConstructUseTypeEnum.nan: return false; } @@ -318,6 +326,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incMM: case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.em: + case ConstructUseTypeEnum.click: return LearningSkillsEnum.reading; case ConstructUseTypeEnum.pvm: return LearningSkillsEnum.speaking; @@ -364,6 +373,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ignL: case ConstructUseTypeEnum.ignM: case ConstructUseTypeEnum.ignMM: + case ConstructUseTypeEnum.click: case ConstructUseTypeEnum.nan: return null; } diff --git a/lib/pangea/toolbar/utils/token_rendering_util.dart b/lib/pangea/toolbar/utils/token_rendering_util.dart index 8efbeeedc..f5eae4603 100644 --- a/lib/pangea/toolbar/utils/token_rendering_util.dart +++ b/lib/pangea/toolbar/utils/token_rendering_util.dart @@ -1,8 +1,8 @@ -import 'package:flutter/material.dart'; - +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:flutter/material.dart'; class TokenRenderingUtil { final PangeaMessageEvent? pangeaMessageEvent; @@ -85,7 +85,10 @@ class TokenRenderingUtil { } } - Color backgroundColor(BuildContext context, bool selected) { + Color backgroundColor(BuildContext context, bool selected, bool isNew) { + if (isNew) { + return AppConfig.success; + } return selected ? Theme.of(context).colorScheme.primary : Colors.white.withAlpha(0); diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index daaecd2ae..93d0ea081 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -2,15 +2,13 @@ import 'dart:async'; import 'dart:developer'; import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; - import 'package:collection/collection.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; @@ -35,6 +33,10 @@ import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_sel import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart'; import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:matrix/matrix.dart'; /// Controls data at the top level of the toolbar (mainly token / toolbar mode selection) class MessageSelectionOverlay extends StatefulWidget { @@ -103,6 +105,8 @@ class MessageOverlayController extends State double maxWidth = AppConfig.toolbarMinWidth; + List newTokens = []; + ///////////////////////////////////// /// Lifecycle ///////////////////////////////////// @@ -114,6 +118,12 @@ class MessageOverlayController extends State WidgetsBinding.instance.addPostFrameCallback( (_) => widget.chatController.setSelectedEvent(event), ); + newTokens = pangeaMessageEvent?.messageDisplayRepresentation?.tokens + ?.where((token) { + return token.lemma.saveVocab == true && + token.vocabConstruct.uses.isEmpty; + }).toList() ?? + []; } @override @@ -569,6 +579,42 @@ class MessageOverlayController extends State } updateSelectedSpan(token.text); + + Future.delayed(const Duration(seconds: 2), () { + if (isNewToken(token)) { + MatrixState.pangeaController.putAnalytics.setState( + AnalyticsStream( + eventId: event.eventId, + roomId: event.room.id, + constructs: [ + OneConstructUse( + useType: ConstructUseTypeEnum.click, + lemma: token.lemma.text, + constructType: ConstructTypeEnum.vocab, + metadata: ConstructUseMetaData( + roomId: event.room.id, + timeStamp: DateTime.now(), + eventId: event.eventId, + ), + category: token.pos, + form: token.text.content, + xp: ConstructUseTypeEnum.click.pointValue, + ), + ], + targetID: token.text.uniqueKey, + ), + ); + // Remove the token from newTokens so it is no longer highlighted as "new" + setState(() { + newTokens.removeWhere( + (t) => + t.text.offset == token.text.offset && + t.text.length == token.text.length, + ); + debugPrint("$token.text has been removed from newTokens list."); + }); + } + }); } /// Whether the given token is currently selected or highlighted @@ -578,6 +624,15 @@ class MessageOverlayController extends State return isSelected; } + bool isNewToken(PangeaToken token) { + if (newTokens.isEmpty) return false; + return newTokens.any( + (t) => + t.text.offset == token.text.offset && + t.text.length == token.text.length, + ); + } + bool isTokenHighlighted(PangeaToken token) { if (_highlightedTokens == null) return false; return _highlightedTokens!.any( diff --git a/lib/pangea/toolbar/widgets/reading_assistance_content.dart b/lib/pangea/toolbar/widgets/reading_assistance_content.dart index 0edc58849..8cfe06562 100644 --- a/lib/pangea/toolbar/widgets/reading_assistance_content.dart +++ b/lib/pangea/toolbar/widgets/reading_assistance_content.dart @@ -1,9 +1,5 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix_api_lite/model/message_types.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -16,6 +12,8 @@ import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_act import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix_api_lite/model/message_types.dart'; const double minCardHeight = 70; @@ -120,6 +118,8 @@ class ReadingAssistanceContentState extends State { token: widget.overlayController.selectedToken!, messageEvent: widget.overlayController.pangeaMessageEvent!, overlayController: widget.overlayController, + wordIsNew: widget.overlayController + .isNewToken(widget.overlayController.selectedToken!), ); } } diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart new file mode 100644 index 000000000..c5222daeb --- /dev/null +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -0,0 +1,121 @@ +import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; +import 'package:flutter/material.dart'; + +class NewWordOverlay extends StatefulWidget { + final Widget child; + final bool show; + final Color overlayColor; + final VoidCallback? onComplete; + + const NewWordOverlay({ + super.key, + required this.child, + required this.show, + required this.overlayColor, + this.onComplete, + }); + + @override + State createState() => _NewWordOverlayState(); +} + +class _NewWordOverlayState extends State + with TickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _xpScaleAnim; + late final Animation _fadeAnim; + late final Animation _alignmentAnim; + late final Animation _offsetAnim; + final bool _showOverlay = true; + Widget xpSeedWidget = const SizedBox(); + Widget? get svg => ConstructLevelEnum.seeds.icon(); + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2500), + ); + + _xpScaleAnim = CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.bounceOut), + ); + _fadeAnim = CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeOut), + ); + + // Alignment animation: stays center until 0.5, then animates to topRight + _alignmentAnim = AlignmentTween( + begin: Alignment.center, + end: Alignment.topRight, + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), + ), + ); + + // Offset animation: stays at Offset.zero, then moves up and right + _offsetAnim = Tween( + begin: Offset.zero, + end: const Offset(-0.1, -0.1), + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), + ), + ); + + xpSeedWidget = Container( + child: svg, + ); + + if (mounted) { + _controller.forward(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!_showOverlay) return widget.child; + return Stack( + children: [ + widget.child, + Positioned.fill( + child: FadeTransition( + opacity: ReverseAnimation(_fadeAnim), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + color: widget.overlayColor, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Align( + alignment: _alignmentAnim.value, + child: FractionalTranslation( + translation: _offsetAnim.value, + child: ScaleTransition( + scale: _xpScaleAnim, + child: xpSeedWidget, + ), + ), + ); + }, + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index dcb86b6eb..da137881b 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; @@ -15,18 +13,22 @@ import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; class WordZoomWidget extends StatelessWidget { final PangeaToken token; final PangeaMessageEvent messageEvent; final MessageOverlayController overlayController; + final bool wordIsNew; const WordZoomWidget({ super.key, required this.token, required this.messageEvent, required this.overlayController, + required this.wordIsNew, }); PangeaToken get _selectedToken => overlayController.selectedToken!; @@ -41,7 +43,9 @@ class WordZoomWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + final isNewWord = wordIsNew; + final overlayColor = Theme.of(context).scaffoldBackgroundColor; + Widget card = Container( padding: const EdgeInsets.all(12.0), constraints: const BoxConstraints( minHeight: AppConfig.toolbarMinHeight - 8, @@ -241,5 +245,13 @@ class WordZoomWidget extends StatelessWidget { ), ), ); + if (isNewWord) { + card = NewWordOverlay( + show: true, + overlayColor: overlayColor, + child: card, + ); + } + return card; } }