diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 139380aa0..ac3ca9fca 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -31,7 +31,6 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_controller.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; -import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/analytics_misc/message_analytics_feedback.dart'; @@ -469,21 +468,9 @@ class ChatController extends State } void _onAnalyticsUpdate(AnalyticsStreamUpdate update) { - if (update.targetID == null) return; - OverlayUtil.showOverlay( - overlayKey: "${update.targetID ?? ""}_points", - followerAnchor: Alignment.bottomCenter, - targetAnchor: Alignment.bottomCenter, - context: context, - child: PointsGainedAnimation( - points: update.points, - targetID: update.targetID!, - ), - transformTargetId: update.targetID ?? "", - closePrevOverlay: false, - backDropToDismiss: false, - ignorePointer: true, - ); + if (update.targetID != null) { + OverlayUtil.showPointsGained(update.targetID!, context); + } } Future _botAudioListener(SyncUpdate update) async { diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 1103bfc0d..48a2fca5b 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -442,12 +442,12 @@ class HtmlMessage extends StatelessWidget { children: [ if (token != null && overlayController != null) TokenEmojiButton( + token: token, enabled: token.lemma.saveVocab, - emoji: token.vocabConstructID.userSetEmoji.firstOrNull, targetId: overlayController!.tokenEmojiPopupKey(token), - onSelect: () => - overlayController!.showTokenEmojiPopup(token), selectModeNotifier: overlayController!.selectedMode, + selectedTokenNotifier: + overlayController!.selectedTokenNotifier, ), if (renderer.showCenterStyling && token != null && @@ -946,6 +946,8 @@ class HtmlMessage extends StatelessWidget { // Use TokenEmojiButton to ensure consistent vertical alignment for non-token elements (e.g., emojis) in practice mode. TokenEmojiButton( selectModeNotifier: overlayController!.selectedMode, + selectedTokenNotifier: + overlayController!.selectedTokenNotifier, enabled: false, ), RichText( diff --git a/lib/pangea/analytics_misc/lemma_emoji_setter_mixin.dart b/lib/pangea/analytics_misc/lemma_emoji_setter_mixin.dart new file mode 100644 index 000000000..bccb467a3 --- /dev/null +++ b/lib/pangea/analytics_misc/lemma_emoji_setter_mixin.dart @@ -0,0 +1,54 @@ +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/constructs/construct_identifier.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +mixin LemmaEmojiSetter { + Future setLemmaEmoji( + ConstructIdentifier constructId, + String emoji, + String? targetId, + ) async { + if (constructId.userSetEmoji.isEmpty) { + _sendEmojiAnalytics( + constructId, + targetId: targetId, + ); + } + + await constructId.setUserLemmaInfo( + constructId.userLemmaInfo.copyWith(emojis: [emoji]), + ); + } + + void _sendEmojiAnalytics( + ConstructIdentifier constructId, { + String? eventId, + String? roomId, + String? targetId, + }) { + MatrixState.pangeaController.putAnalytics.setState( + AnalyticsStream( + eventId: eventId, + roomId: roomId, + targetID: targetId, + constructs: [ + OneConstructUse( + useType: ConstructUseTypeEnum.em, + lemma: constructId.lemma, + constructType: constructId.type, + metadata: ConstructUseMetaData( + roomId: roomId, + timeStamp: DateTime.now(), + eventId: eventId, + ), + category: constructId.category, + form: constructId.lemma, + xp: ConstructUseTypeEnum.em.pointValue, + ), + ], + ), + ); + } +} diff --git a/lib/pangea/common/utils/any_state_holder.dart b/lib/pangea/common/utils/any_state_holder.dart index 30e10d502..a2f8d4c61 100644 --- a/lib/pangea/common/utils/any_state_holder.dart +++ b/lib/pangea/common/utils/any_state_holder.dart @@ -84,6 +84,7 @@ class PangeaAnyState { if (entry != null) { try { entry.entry.remove(); + entry.entry.dispose(); } catch (err, s) { ErrorHandler.logError( e: err, @@ -117,6 +118,7 @@ class PangeaAnyState { for (int i = 0; i < shouldRemove.length; i++) { try { shouldRemove[i].entry.remove(); + shouldRemove[i].entry.dispose(); } catch (err, s) { ErrorHandler.logError( e: err, diff --git a/lib/pangea/common/utils/overlay.dart b/lib/pangea/common/utils/overlay.dart index 490717131..eb10ac357 100644 --- a/lib/pangea/common/utils/overlay.dart +++ b/lib/pangea/common/utils/overlay.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart'; import 'package:fluffychat/pangea/choreographer/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/choreographer.dart'; @@ -284,4 +285,24 @@ class OverlayUtil { ), ); } + + static void showPointsGained( + String targetId, + BuildContext context, + ) { + showOverlay( + overlayKey: "${targetId}_points", + followerAnchor: Alignment.bottomCenter, + targetAnchor: Alignment.bottomCenter, + context: context, + child: PointsGainedAnimation( + points: 2, + targetID: targetId, + ), + transformTargetId: targetId, + closePrevOverlay: false, + backDropToDismiss: false, + ignorePointer: true, + ); + } } diff --git a/lib/pangea/constructs/construct_identifier.dart b/lib/pangea/constructs/construct_identifier.dart index 5d7174aef..c23d821bc 100644 --- a/lib/pangea/constructs/construct_identifier.dart +++ b/lib/pangea/constructs/construct_identifier.dart @@ -8,9 +8,6 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.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/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; @@ -138,6 +135,9 @@ class ConstructIdentifier { ); } + bool get isContentWord => + PartOfSpeechEnumExtensions.fromString(category)?.isContentWord ?? false; + ConstructUses get constructUses => MatrixState.pangeaController.getAnalytics.constructListModel .getConstructUses( @@ -150,75 +150,41 @@ class ConstructIdentifier { uses: [], ); - List get userSetEmoji => userLemmaInfo?.emojis ?? []; + LemmaInfoRequest get _lemmaInfoRequest => LemmaInfoRequest( + partOfSpeech: category, + lemmaLang: MatrixState + .pangeaController.languageController.userL2?.langCodeShort ?? + LanguageKeys.defaultLanguage, + userL1: MatrixState + .pangeaController.languageController.userL1?.langCodeShort ?? + LanguageKeys.defaultLanguage, + lemma: lemma, + ); - UserSetLemmaInfo? get userLemmaInfo { + /// [lemmmaLang] if not set, assumed to be userL2 + Future getLemmaInfo() => LemmaInfoRepo.get( + _lemmaInfoRequest, + ); + + List get userSetEmoji => userLemmaInfo.emojis ?? []; + + UserSetLemmaInfo get userLemmaInfo { switch (type) { case ConstructTypeEnum.vocab: return MatrixState.pangeaController.matrixState.client - .analyticsRoomLocal() - ?.getUserSetLemmaInfo(this); + .analyticsRoomLocal() + ?.getUserSetLemmaInfo(this) ?? + UserSetLemmaInfo(); case ConstructTypeEnum.morph: debugger(when: kDebugMode); ErrorHandler.logError( e: Exception("Morphs should not have userSetEmoji"), data: toJson(), ); - return null; + return UserSetLemmaInfo(); } } - /// Sets emoji and awards XP if it's a NEW emoji selection or from game - Future setEmojiWithXP({ - required String emoji, - bool isFromCorrectAnswer = false, - String? eventId, - String? roomId, - }) async { - final hadEmojiPreviously = userSetEmoji.isNotEmpty; - //correct answers already award xp so we don't here, but we do still need to set the emoji if it isn't already set - final shouldAwardXP = !hadEmojiPreviously && !isFromCorrectAnswer; - - //Set emoji representation - await setUserLemmaInfo(UserSetLemmaInfo(emojis: [emoji])); - - if (shouldAwardXP) { - await _recordEmojiAnalytics( - eventId: eventId, - roomId: roomId, - ); - } - } - - Future _recordEmojiAnalytics({ - String? eventId, - String? roomId, - }) async { - const useType = ConstructUseTypeEnum.em; - - MatrixState.pangeaController.putAnalytics.setState( - AnalyticsStream( - eventId: eventId, - roomId: roomId, - constructs: [ - OneConstructUse( - useType: useType, - lemma: lemma, - constructType: type, - metadata: ConstructUseMetaData( - roomId: roomId, - timeStamp: DateTime.now(), - eventId: eventId, - ), - category: category, - form: lemma, - xp: useType.pointValue, - ), - ], - ), - ); - } - Future setUserLemmaInfo(UserSetLemmaInfo newLemmaInfo) async { final client = MatrixState.pangeaController.matrixState.client; final l2 = MatrixState.pangeaController.languageController.userL2; @@ -239,23 +205,4 @@ class ConstructIdentifier { ); } } - - LemmaInfoRequest get _lemmaInfoRequest => LemmaInfoRequest( - partOfSpeech: category, - lemmaLang: MatrixState - .pangeaController.languageController.userL2?.langCodeShort ?? - LanguageKeys.defaultLanguage, - userL1: MatrixState - .pangeaController.languageController.userL1?.langCodeShort ?? - LanguageKeys.defaultLanguage, - lemma: lemma, - ); - - /// [lemmmaLang] if not set, assumed to be userL2 - Future getLemmaInfo() => LemmaInfoRepo.get( - _lemmaInfoRequest, - ); - - bool get isContentWord => - PartOfSpeechEnumExtensions.fromString(category)?.isContentWord ?? false; } diff --git a/lib/pangea/events/models/pangea_token_model.dart b/lib/pangea/events/models/pangea_token_model.dart index 6081fa585..b03d2f550 100644 --- a/lib/pangea/events/models/pangea_token_model.dart +++ b/lib/pangea/events/models/pangea_token_model.dart @@ -11,7 +11,6 @@ import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/constructs/construct_form.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; -import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/morphs/morph_repo.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; @@ -241,9 +240,6 @@ class PangeaToken { ConstructForm get vocabForm => ConstructForm(form: text.content, cId: vocabConstructID); - Future setEmoji(List emojis) => - vocabConstructID.setUserLemmaInfo(UserSetLemmaInfo(emojis: emojis)); - Set morphActivityDistractors( MorphFeaturesEnum morphFeature, String morphTag, diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart index 2a882d407..3109a851e 100644 --- a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart +++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart @@ -7,7 +7,8 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; +import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart'; +import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.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'; @@ -28,74 +29,70 @@ class LemmaHighlightEmojiRow extends StatefulWidget { LemmaHighlightEmojiRowState createState() => LemmaHighlightEmojiRowState(); } -class LemmaHighlightEmojiRowState extends State { - String? displayEmoji; +class LemmaHighlightEmojiRowState extends State + with LemmaEmojiSetter { bool _showShimmer = true; - bool _hasShimmered = false; + String? _selectedEmoji; + + late StreamSubscription _analyticsSubscription; + Timer? _shimmerTimer; @override void initState() { super.initState(); - displayEmoji = widget.cId.userSetEmoji.firstOrNull; - _showShimmer = (displayEmoji == null); - } - - void _startShimmer() { - if (!widget.controller.isLoading && _showShimmer) { - Future.delayed(const Duration(milliseconds: 1500), () { - if (mounted) { - setState(() => _showShimmer = false); - setState(() => _hasShimmered = true); - } - }); - } + _analyticsSubscription = MatrixState + .pangeaController.getAnalytics.analyticsStream.stream + .listen(_onAnalyticsUpdate); + _setShimmer(); } @override - didUpdateWidget(LemmaHighlightEmojiRow oldWidget) { - //Reset shimmer state for diff constructs in 2 column mode - if (oldWidget.cId != widget.cId) { - setState(() { - displayEmoji = widget.cId.userSetEmoji.firstOrNull; - _showShimmer = (displayEmoji == null); - _hasShimmered = false; - }); - } + void didUpdateWidget(LemmaHighlightEmojiRow oldWidget) { + if (oldWidget.cId != widget.cId) _setShimmer(); super.didUpdateWidget(oldWidget); } @override void dispose() { + _analyticsSubscription.cancel(); + _shimmerTimer?.cancel(); super.dispose(); } - String transformTargetId(String emoji) => - "emoji-choice-item-$emoji-${widget.cId.lemma}"; + void _setShimmer() { + setState(() { + _selectedEmoji = widget.cId.userSetEmoji.firstOrNull; + _showShimmer = _selectedEmoji == null; - Future setEmoji(String emoji, BuildContext context) async { - try { - final String? userSetEmoji = widget.cId.userSetEmoji.firstOrNull; - setState(() => displayEmoji = emoji); - await widget.cId.setEmojiWithXP( - emoji: emoji, - isFromCorrectAnswer: false, - ); - if (userSetEmoji == null) { - OverlayUtil.showOverlay( - overlayKey: "${transformTargetId(emoji)}_points", - followerAnchor: Alignment.bottomCenter, - targetAnchor: Alignment.bottomCenter, - context: context, - child: PointsGainedAnimation( - points: 2, - targetID: transformTargetId(emoji), - ), - transformTargetId: transformTargetId(emoji), - closePrevOverlay: false, - backDropToDismiss: false, - ignorePointer: true, - ); + if (_showShimmer) { + _shimmerTimer?.cancel(); + _shimmerTimer = Timer(const Duration(milliseconds: 1500), () { + if (mounted) { + setState(() { + _showShimmer = false; + _shimmerTimer?.cancel(); + _shimmerTimer = null; + }); + } + }); } + }); + } + + void _onAnalyticsUpdate(AnalyticsStreamUpdate update) { + if (update.targetID != null) { + OverlayUtil.showPointsGained(update.targetID!, context); + } + } + + Future _setEmoji(String emoji, BuildContext context) async { + try { + setState(() => _selectedEmoji = emoji); + await setLemmaEmoji( + widget.cId, + emoji, + "emoji-choice-item-$emoji-${widget.cId.lemma}", + ); } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s); @@ -107,7 +104,6 @@ class LemmaHighlightEmojiRowState extends State { if (widget.controller.isLoading) { return const CircularProgressIndicator.adaptive(); } - _startShimmer(); final emojis = widget.controller.lemmaInfo?.emoji; if (widget.controller.error != null || emojis == null || emojis.isEmpty) { @@ -129,10 +125,11 @@ class LemmaHighlightEmojiRowState extends State { .map( (emoji) => EmojiChoiceItem( emoji: emoji, - onSelectEmoji: () => setEmoji(emoji, context), - isDisplay: (displayEmoji == emoji), - showShimmer: (_showShimmer && !_hasShimmered), - transformTargetId: transformTargetId(emoji), + onSelectEmoji: () => _setEmoji(emoji, context), + isDisplay: _selectedEmoji == emoji, + showShimmer: _showShimmer, + transformTargetId: + "emoji-choice-item-$emoji-${widget.cId.lemma}", ), ) .toList(), diff --git a/lib/pangea/lemmas/user_set_lemma_info.dart b/lib/pangea/lemmas/user_set_lemma_info.dart index 1387c89b9..1931f6b48 100644 --- a/lib/pangea/lemmas/user_set_lemma_info.dart +++ b/lib/pangea/lemmas/user_set_lemma_info.dart @@ -23,6 +23,16 @@ class UserSetLemmaInfo { }; } + UserSetLemmaInfo copyWith({ + List? emojis, + String? meaning, + }) { + return UserSetLemmaInfo( + emojis: emojis ?? this.emojis, + meaning: meaning ?? this.meaning, + ); + } + @override bool operator ==(Object other) => identical(this, other) || diff --git a/lib/pangea/message_token_text/token_emoji_button.dart b/lib/pangea/message_token_text/token_emoji_button.dart index e1fab172e..f0e1210a8 100644 --- a/lib/pangea/message_token_text/token_emoji_button.dart +++ b/lib/pangea/message_token_text/token_emoji_button.dart @@ -1,23 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_emoji_picker.dart'; import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; class TokenEmojiButton extends StatefulWidget { final ValueNotifier selectModeNotifier; - final bool enabled; - final String? emoji; + final ValueNotifier selectedTokenNotifier; + + final PangeaToken? token; final String? targetId; - final VoidCallback? onSelect; + final bool enabled; const TokenEmojiButton({ super.key, required this.selectModeNotifier, - this.enabled = true, - this.emoji, + required this.selectedTokenNotifier, + this.token, this.targetId, - this.onSelect, + this.enabled = true, }); @override @@ -25,24 +33,30 @@ class TokenEmojiButton extends StatefulWidget { } class TokenEmojiButtonState extends State - with TickerProviderStateMixin { + with TickerProviderStateMixin, LemmaEmojiSetter { final double buttonSize = 20.0; SelectMode? _prevMode; AnimationController? _controller; Animation? _sizeAnimation; + String? _emoji; + @override void initState() { super.initState(); + _emoji = widget.token?.vocabConstructID.userSetEmoji.firstOrNull; + _initAnimation(); _prevMode = widget.selectModeNotifier.value; widget.selectModeNotifier.addListener(_onUpdateSelectMode); + widget.selectedTokenNotifier.addListener(_onSelectToken); } @override void dispose() { _controller?.dispose(); widget.selectModeNotifier.removeListener(_onUpdateSelectMode); + widget.selectedTokenNotifier.removeListener(_onSelectToken); super.dispose(); } @@ -73,6 +87,69 @@ class TokenEmojiButtonState extends State _prevMode = mode; } + void _onSelectToken() { + final selected = widget.selectedTokenNotifier.value; + if (selected != null && selected == widget.token) { + showTokenEmojiPopup(); + } + } + + void showTokenEmojiPopup() { + if (widget.targetId == null || widget.token == null) return; + OverlayUtil.showPositionedCard( + overlayKey: "overlay_emoji_selector", + context: context, + cardToShow: LemmaMeaningBuilder( + langCode: + MatrixState.pangeaController.languageController.activeL2Code()!, + constructId: widget.token!.vocabConstructID, + builder: (context, controller) { + return Material( + type: MaterialType.transparency, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + child: LemmaEmojiPicker( + emojis: controller.lemmaInfo?.emoji ?? [], + onSelect: (emoji) { + _setTokenEmoji(emoji); + MatrixState.pAnyState.closeOverlay("overlay_emoji_selector"); + }, + loading: controller.isLoading, + ), + ), + ); + }, + ), + transformTargetId: widget.targetId!, + closePrevOverlay: false, + addBorder: false, + maxWidth: (40 * 5) + (4 * 5) + 16, + maxHeight: 60, + ); + } + + void _setTokenEmoji(String emoji) { + setState(() => _emoji = emoji); + + if (widget.targetId == null || widget.token == null) return; + setLemmaEmoji( + widget.token!.vocabConstructID, + emoji, + widget.targetId, + ).catchError((e, s) { + ErrorHandler.logError( + data: widget.token!.toJson(), + e: e, + s: s, + ); + }); + } + @override Widget build(BuildContext context) { if (_sizeAnimation == null) { @@ -81,11 +158,11 @@ class TokenEmojiButtonState extends State final child = widget.enabled ? InkWell( - onTap: widget.onSelect, + onTap: showTokenEmojiPopup, borderRadius: BorderRadius.circular(99.0), - child: widget.emoji != null + child: _emoji != null ? Text( - widget.emoji!, + _emoji!, style: TextStyle(fontSize: buttonSize - 4.0), textScaler: TextScaler.noScaling, ) diff --git a/lib/pangea/toolbar/reading_assistance_input_row/word_emoji_choice.dart b/lib/pangea/toolbar/reading_assistance_input_row/word_emoji_choice.dart deleted file mode 100644 index 1e10e9af4..000000000 --- a/lib/pangea/toolbar/reading_assistance_input_row/word_emoji_choice.dart +++ /dev/null @@ -1,132 +0,0 @@ -// 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/choreographer/widgets/choice_array.dart'; -// import 'package:fluffychat/pangea/choreographer/widgets/it_shimmer.dart'; -// import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; -// import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; -// import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; -// import 'package:fluffychat/widgets/matrix.dart'; -// import 'package:flutter/material.dart'; -// import 'package:fluffychat/l10n/l10n.dart'; - -// class WordEmojiChoice extends StatefulWidget { -// const WordEmojiChoice({ -// super.key, -// required this.constructID, -// required this.onEmojiChosen, -// required this.form, -// this.roomId, -// this.eventId, -// }); - -// final ConstructIdentifier constructID; -// final String form; -// final String? roomId; -// final String? eventId; -// final void Function() onEmojiChosen; - -// @override -// WordEmojiChoiceState createState() => WordEmojiChoiceState(); -// } - -// class WordEmojiChoiceState extends State { -// String? localSelected; - -// @override -// void initState() { -// super.initState(); -// localSelected = widget.constructID.userSetEmoji.single; -// } - -// Future onChoice(BuildContext context, emoji) async { -// setState(() => localSelected = emoji); - -// MatrixState.pangeaController.putAnalytics.setState( -// AnalyticsStream( -// eventId: widget.eventId, -// roomId: widget.roomId, -// constructs: [ -// OneConstructUse( -// useType: ConstructUseTypeEnum.em, -// lemma: widget.constructID.lemma, -// constructType: ConstructTypeEnum.vocab, -// metadata: ConstructUseMetaData( -// roomId: widget.roomId, -// timeStamp: DateTime.now(), -// eventId: widget.eventId, -// ), -// category: widget.constructID.category, -// form: widget.form, -// ), -// ], -// origin: AnalyticsUpdateOrigin.wordZoom, -// ), -// ); - -// await widget.constructID.setEmoji(emoji); - -// await Future.delayed( -// const Duration(milliseconds: choiceArrayAnimationDuration), -// ); - -// widget.onEmojiChosen(); - -// setState(() => {}); -// } - -// @override -// Widget build(BuildContext context) { -// return SingleChildScrollView( -// child: Column( -// mainAxisAlignment: MainAxisAlignment.center, -// mainAxisSize: MainAxisSize.max, -// children: [ -// FutureBuilder( -// future: widget.constructID.getEmojiChoices(), -// builder: (context, snapshot) { -// if (snapshot.hasError) { -// return Text(L10n.of(context).oopsSomethingWentWrong); -// } - -// if (snapshot.connectionState == ConnectionState.waiting || -// snapshot.data == null) { -// return const ItShimmer(originalSpan: "😀", fontSize: 26); -// } - -// return ChoicesArray( -// isLoading: snapshot.connectionState == ConnectionState.waiting, -// choices: snapshot.data! -// .map( -// (emoji) => Choice( -// color: localSelected == emoji -// ? Theme.of(context).colorScheme.primary -// : Colors.transparent, -// text: emoji, -// isGold: localSelected == emoji, -// ), -// ) -// .toList(), -// onPressed: (emoji, index) => onChoice(context, emoji), -// originalSpan: "😀", -// uniqueKeyForLayerLink: (int index) => "emojiChoice$index", -// selectedChoiceIndex: snapshot.data!.indexWhere( -// (element) => element == widget.constructID.userSetEmoji, -// ), -// tts: null, -// fontSize: 26, -// enableMultiSelect: true, -// isActive: true, -// overflowMode: OverflowMode.horizontalScroll, -// ); -// }, -// ), -// const InstructionsInlineTooltip( -// instructionsEnum: InstructionsEnum.chooseEmoji, -// ), -// ], -// ), -// ); -// } -// } diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 45e990ec2..e47b7eabd 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -15,20 +15,16 @@ 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/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; -import 'package:fluffychat/pangea/lemmas/lemma_emoji_picker.dart'; import 'package:fluffychat/pangea/message_token_text/tokens_util.dart'; import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_controller.dart'; import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart'; import 'package:fluffychat/pangea/toolbar/widgets/select_mode_controller.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; /// Controls data at the top level of the toolbar (mainly token / toolbar mode selection) @@ -63,6 +59,7 @@ class MessageOverlayController extends State Event get event => widget._event; PangeaTokenText? _selectedSpan; + ValueNotifier selectedTokenNotifier = ValueNotifier(null); List? _highlightedTokens; @@ -96,6 +93,7 @@ class MessageOverlayController extends State ); selectModeController.dispose(); practiceController.dispose(); + selectedTokenNotifier.dispose(); super.dispose(); } @@ -201,17 +199,35 @@ class MessageOverlayController extends State } if (selectedSpan == _selectedSpan) return; - // if (selectedMorph != null) { - // selectedMorph = null; - // } - _selectedSpan = selectedSpan; - if (selectedMode.value == SelectMode.emoji && selectedToken != null) { - showTokenEmojiPopup(selectedToken!); - } + selectedTokenNotifier.value = selectedToken; if (mounted) { setState(() {}); - if (selectedToken != null) _onSelectNewToken(selectedToken!); + if (selectedToken != null && isNewToken(selectedToken!)) { + final token = selectedToken!; + 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: "word-zoom-card-${token.text.uniqueKey}", + ), + ); + } } } @@ -303,32 +319,6 @@ class MessageOverlayController extends State updateSelectedSpan(token.text); } - void _onSelectNewToken(PangeaToken token) { - if (!isNewToken(token)) return; - 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: "word-zoom-card-${token.text.uniqueKey}", - ), - ); - } - /// Whether the given token is currently selected or highlighted bool isTokenSelected(PangeaToken token) { final isSelected = _selectedSpan?.offset == token.text.offset && @@ -346,58 +336,6 @@ class MessageOverlayController extends State ); } - void showTokenEmojiPopup( - PangeaToken token, - ) { - OverlayUtil.showPositionedCard( - overlayKey: "overlay_emoji_selector_${event.eventId}", - context: context, - cardToShow: LemmaMeaningBuilder( - langCode: - MatrixState.pangeaController.languageController.activeL2Code()!, - constructId: token.vocabConstructID, - builder: (context, controller) { - return Material( - type: MaterialType.transparency, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - ), - child: LemmaEmojiPicker( - emojis: controller.lemmaInfo?.emoji ?? [], - onSelect: (emoji) async { - final resp = await showFutureLoadingDialog( - context: context, - future: () => _setTokenEmoji(token, emoji), - ); - if (mounted && !resp.isError) { - MatrixState.pAnyState.closeOverlay( - "overlay_emoji_selector_${event.eventId}", - ); - } - }, - loading: controller.isLoading, - ), - ), - ); - }, - ), - transformTargetId: tokenEmojiPopupKey(token), - closePrevOverlay: false, - addBorder: false, - maxWidth: (40 * 5) + (4 * 5) + 16, - maxHeight: 60, - ); - } - - Future _setTokenEmoji(PangeaToken token, String emoji) async { - await token.setEmoji([emoji]); - if (mounted) setState(() {}); - } - String tokenEmojiPopupKey(PangeaToken token) => "${token.uniqueId}_${event.eventId}_emoji_button"; diff --git a/lib/pangea/toolbar/widgets/practice_controller.dart b/lib/pangea/toolbar/widgets/practice_controller.dart index 83c24880d..7df582262 100644 --- a/lib/pangea/toolbar/widgets/practice_controller.dart +++ b/lib/pangea/toolbar/widgets/practice_controller.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; @@ -118,6 +117,9 @@ class PracticeController with ChangeNotifier { ? _activity!.onMultipleChoiceSelect(token, choice) : _activity!.onMatch(token, choice); + final targetId = + "message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}"; + // we don't take off points for incorrect emoji matches if (_activity!.activityType != ActivityTypeEnum.emoji || isCorrect) { final constructUseType = _activity!.practiceTarget.record.responses.last @@ -143,25 +145,25 @@ class PracticeController with ChangeNotifier { xp: constructUseType.pointValue, ), ], - targetID: - "message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}", + targetID: targetId, ), ); } if (isCorrect) { if (_activity!.activityType == ActivityTypeEnum.emoji) { - choice.form.cId.setEmojiWithXP( - emoji: choice.choiceContent, - isFromCorrectAnswer: true, - eventId: pangeaMessageEvent.eventId, - roomId: pangeaMessageEvent.room.id, + choice.form.cId.setUserLemmaInfo( + choice.form.cId.userLemmaInfo.copyWith( + emojis: [choice.choiceContent], + ), ); } if (_activity!.activityType == ActivityTypeEnum.wordMeaning) { choice.form.cId.setUserLemmaInfo( - UserSetLemmaInfo(meaning: choice.choiceContent), + choice.form.cId.userLemmaInfo.copyWith( + meaning: choice.choiceContent, + ), ); } }