From b795ba3c065e711788709ddbcbce17672c982bc2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 16 Dec 2025 16:01:40 -0500 Subject: [PATCH] use the same widget for word card and vocab details emoji pickers --- .../vocab_analytics_details_view.dart | 235 ++++++++--------- .../vocab_details_emoji_selector.dart | 68 +++++ .../lemmas/lemma_highlight_emoji_row.dart | 246 +++++++++--------- lib/pangea/lemmas/lemma_meaning_widget.dart | 89 ++++--- .../select_mode_controller.dart | 22 +- .../word_card/lemma_reaction_picker.dart | 155 ++++++----- .../word_card/reading_assistance_content.dart | 1 - .../toolbar/word_card/word_zoom_widget.dart | 6 +- 8 files changed, 439 insertions(+), 383 deletions(-) create mode 100644 lib/pangea/analytics_details_popup/vocab_details_emoji_selector.dart diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index e8457ebb4..6f43ba802 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -4,13 +4,12 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart'; +import 'package:fluffychat/pangea/analytics_details_popup/vocab_details_emoji_selector.dart'; import 'package:fluffychat/pangea/analytics_details_popup/word_text_with_audio_button.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/common/widgets/shrinkable_text.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; -import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart'; -import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; import 'package:fluffychat/pangea/lemmas/lemma_meaning_widget.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; @@ -51,146 +50,136 @@ class VocabDetailsView extends StatelessWidget { ? _construct.lemmaCategory.color(context) : _construct.lemmaCategory.darkColor(context)); - return LemmaMeaningBuilder( - langCode: _userL2!, - constructId: _construct.id, - builder: (context, controller) { - return AnalyticsDetailsViewContent( - title: Column( - children: [ - LayoutBuilder( - builder: (context, constraints) { - return ShrinkableText( - text: _construct.lemma, - maxWidth: constraints.maxWidth - 40.0, - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - color: textColor, - ), - ); - }, - ), - if (MatrixState.pangeaController.userController.showTranscription) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: PhoneticTranscriptionWidget( - text: _construct.lemma, - textLanguage: - MatrixState.pangeaController.userController.userL2!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: textColor.withAlpha((0.7 * 255).toInt()), - fontSize: 18, - ), - iconSize: _iconSize * 0.8, - ), - ), - ], - ), - subtitle: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - Text( - getGrammarCopy( - category: "POS", - lemma: _construct.category, - context: context, - ) ?? - _construct.lemma, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: textColor, - ), - ), - SizedBox( - width: _iconSize, - height: _iconSize, - child: MorphIcon( - morphFeature: MorphFeaturesEnum.Pos, - morphTag: _construct.category, + return AnalyticsDetailsViewContent( + title: Column( + children: [ + LayoutBuilder( + builder: (context, constraints) { + return ShrinkableText( + text: _construct.lemma, + maxWidth: constraints.maxWidth - 40.0, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: textColor, ), - ), - ], + ); + }, + ), + if (MatrixState.pangeaController.userController.showTranscription) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: PhoneticTranscriptionWidget( + text: _construct.lemma, + textLanguage: + MatrixState.pangeaController.userController.userL2!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: textColor.withAlpha((0.7 * 255).toInt()), + fontSize: 18, + ), + iconSize: _iconSize * 0.8, ), - const SizedBox(height: 16.0), + ), + ], + ), + subtitle: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ Text( - L10n.of(context).vocabEmoji, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - fontStyle: FontStyle.italic, + getGrammarCopy( + category: "POS", + lemma: _construct.category, + context: context, + ) ?? + _construct.lemma, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: textColor, ), ), - LemmaHighlightEmojiRow( - controller: controller, - cId: constructId, + SizedBox( + width: _iconSize, + height: _iconSize, + child: MorphIcon( + morphFeature: MorphFeaturesEnum.Pos, + morphTag: _construct.category, + ), ), ], ), - headerContent: Padding( - padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), - child: Column( - spacing: 8.0, - children: [ - Align( - alignment: Alignment.topLeft, - child: _userL2 == null - ? Text(L10n.of(context).meaningNotFound) - : LemmaMeaningWidget( - controller: controller, - constructUse: _construct, - style: Theme.of(context).textTheme.bodyLarge, - leading: TextSpan( - text: L10n.of(context).meaningSectionHeader, - style: - Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), + const SizedBox(height: 16.0), + Text( + L10n.of(context).vocabEmoji, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontStyle: FontStyle.italic, + color: textColor, ), - Align( - alignment: Alignment.topLeft, - child: Wrap( - alignment: WrapAlignment.start, - runAlignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - L10n.of(context).formSectionHeader, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: VocabDetailsEmojiSelector(constructId), + ), + ], + ), + headerContent: Padding( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: Column( + spacing: 8.0, + children: [ + Align( + alignment: Alignment.topLeft, + child: _userL2 == null + ? Text(L10n.of(context).meaningNotFound) + : LemmaMeaningWidget( + constructId: constructId, + style: Theme.of(context).textTheme.bodyLarge, + leading: TextSpan( + text: L10n.of(context).meaningSectionHeader, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, ), ), - const SizedBox(width: 6.0), - ...forms.mapIndexed( - (i, form) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - WordTextWithAudioButton( - text: form, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( + ), + ), + Align( + alignment: Alignment.topLeft, + child: Wrap( + alignment: WrapAlignment.start, + runAlignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + L10n.of(context).formSectionHeader, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 6.0), + ...forms.mapIndexed( + (i, form) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + WordTextWithAudioButton( + text: form, + style: + Theme.of(context).textTheme.bodyLarge?.copyWith( color: textColor, ), - uniqueID: "$form-${_construct.lemma}-$i", - langCode: _userL2!, - ), - if (i != forms.length - 1) const Text(", "), - ], + uniqueID: "$form-${_construct.lemma}-$i", + langCode: _userL2!, ), - ), - ], + if (i != forms.length - 1) const Text(", "), + ], + ), ), - ), - ], + ], + ), ), - ), - xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0), - constructId: constructId, - ); - }, + ], + ), + ), + xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0), + constructId: constructId, ); } } diff --git a/lib/pangea/analytics_details_popup/vocab_details_emoji_selector.dart b/lib/pangea/analytics_details_popup/vocab_details_emoji_selector.dart new file mode 100644 index 000000000..85b99b602 --- /dev/null +++ b/lib/pangea/analytics_details_popup/vocab_details_emoji_selector.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class VocabDetailsEmojiSelector extends StatefulWidget { + final ConstructIdentifier constructId; + + const VocabDetailsEmojiSelector( + this.constructId, { + super.key, + }); + + @override + State createState() => + VocabDetailsEmojiSelectorState(); +} + +class VocabDetailsEmojiSelectorState extends State + with LemmaEmojiSetter { + String? selectedEmoji; + + @override + void initState() { + super.initState(); + _setInitialEmoji(); + } + + @override + void didUpdateWidget(covariant VocabDetailsEmojiSelector oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.constructId != widget.constructId) { + _setInitialEmoji(); + } + } + + void _setInitialEmoji() { + setState( + () { + selectedEmoji = widget.constructId.userLemmaInfo.emojis?.firstOrNull; + }, + ); + } + + Future _setEmoji(String emoji) async { + setState(() => selectedEmoji = emoji); + await setLemmaEmoji( + widget.constructId, + emoji, + "emoji-choice-item-$emoji-${widget.constructId.lemma}", + ); + showLemmaEmojiSnackbar(context, widget.constructId, emoji); + } + + @override + Widget build(BuildContext context) { + return LemmaHighlightEmojiRow( + cId: widget.constructId, + langCode: MatrixState.pangeaController.userController.userL2Code!, + emoji: selectedEmoji, + onEmojiSelected: _setEmoji, + ); + } +} diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart index aa22b6b3e..45fe653c7 100644 --- a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart +++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart @@ -1,41 +1,41 @@ import 'dart:async'; -import 'dart:developer'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:fluffychat/config/app_config.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'; import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; +import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; class LemmaHighlightEmojiRow extends StatefulWidget { - final LemmaMeaningBuilderState controller; final ConstructIdentifier cId; + final String langCode; + + final Function(String) onEmojiSelected; + + final String? emoji; + final Widget? selectedEmojiBadge; const LemmaHighlightEmojiRow({ super.key, - required this.controller, required this.cId, + required this.langCode, + required this.onEmojiSelected, + this.emoji, + this.selectedEmojiBadge, }); @override - LemmaHighlightEmojiRowState createState() => LemmaHighlightEmojiRowState(); + State createState() => LemmaHighlightEmojiRowState(); } -class LemmaHighlightEmojiRowState extends State - with LemmaEmojiSetter { - bool _showShimmer = true; - String? _selectedEmoji; - +class LemmaHighlightEmojiRowState extends State { late StreamSubscription _analyticsSubscription; - Timer? _shimmerTimer; @override void initState() { @@ -43,126 +43,115 @@ class LemmaHighlightEmojiRowState extends State _analyticsSubscription = MatrixState .pangeaController.getAnalytics.analyticsStream.stream .listen(_onAnalyticsUpdate); - _setShimmer(); - } - - @override - void didUpdateWidget(LemmaHighlightEmojiRow oldWidget) { - if (oldWidget.cId != widget.cId) _setShimmer(); - super.didUpdateWidget(oldWidget); } @override void dispose() { _analyticsSubscription.cancel(); - _shimmerTimer?.cancel(); super.dispose(); } - void _setShimmer() { - setState(() { - _selectedEmoji = widget.cId.userSetEmoji.firstOrNull; - _showShimmer = _selectedEmoji == null; - - 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!, update.points, context); } } - Future _setEmoji(String emoji, BuildContext context) async { - try { - setState(() => _selectedEmoji = emoji); - await setLemmaEmoji( - widget.cId, - emoji, - "emoji-choice-item-$emoji-${widget.cId.lemma}", - ); - showLemmaEmojiSnackbar(context, widget.cId, emoji); - } catch (e, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s); - } - } - @override Widget build(BuildContext context) { - if (widget.controller.isLoading) { - return const CircularProgressIndicator.adaptive(); - } + return LemmaMeaningBuilder( + langCode: widget.langCode, + constructId: widget.cId, + builder: (context, controller) { + if (controller.isLoading) { + return const CircularProgressIndicator.adaptive(); + } - final emojis = widget.controller.lemmaInfo?.emoji; - if (widget.controller.error != null || emojis == null || emojis.isEmpty) { - return const SizedBox.shrink(); - } + final emojis = controller.lemmaInfo?.emoji; + if (controller.error != null || emojis == null || emojis.isEmpty) { + return const SizedBox.shrink(); + } - return Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: Container( - padding: const EdgeInsets.all(8), - height: 80, - alignment: Alignment.center, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, + return SizedBox( + height: 60.0, child: Row( - mainAxisAlignment: MainAxisAlignment.center, + spacing: 4.0, mainAxisSize: MainAxisSize.min, children: emojis .map( (emoji) => EmojiChoiceItem( + cId: widget.cId, emoji: emoji, - onSelectEmoji: () => _setEmoji(emoji, context), - isDisplay: _selectedEmoji == emoji, - showShimmer: _showShimmer, + onSelectEmoji: () => widget.onEmojiSelected(emoji), + selected: widget.emoji == emoji, transformTargetId: "emoji-choice-item-$emoji-${widget.cId.lemma}", + badge: widget.emoji == emoji + ? widget.selectedEmojiBadge + : null, ), ) .toList(), ), - ), - ), + ); + }, ); } } class EmojiChoiceItem extends StatefulWidget { + final ConstructIdentifier cId; final String emoji; final VoidCallback onSelectEmoji; - final bool isDisplay; - final bool showShimmer; + final bool selected; final String transformTargetId; + final Widget? badge; const EmojiChoiceItem({ super.key, + required this.cId, required this.emoji, - required this.isDisplay, + required this.selected, required this.onSelectEmoji, - required this.showShimmer, required this.transformTargetId, + this.badge, }); @override - EmojiChoiceItemState createState() => EmojiChoiceItemState(); + State createState() => EmojiChoiceItemState(); } class EmojiChoiceItemState extends State { - bool _isHovered = false; + bool shimmer = true; + Timer? _shimmerTimer; + + @override + void initState() { + super.initState(); + _showShimmer(); + } + + @override + void didUpdateWidget(covariant EmojiChoiceItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.emoji != widget.emoji) { + _showShimmer(); + } + } + + @override + void dispose() { + _shimmerTimer?.cancel(); + super.dispose(); + } + + void _showShimmer() { + setState(() => shimmer = true); + _shimmerTimer?.cancel(); + _shimmerTimer = Timer(const Duration(milliseconds: 1500), () { + if (mounted) setState(() => shimmer = false); + }); + } LayerLink get layerLink => MatrixState.pAnyState.layerLinkAndKey(widget.transformTargetId).link; @@ -172,58 +161,59 @@ class EmojiChoiceItemState extends State { final shimmerColor = (Theme.of(context).brightness == Brightness.dark) ? Colors.white : Theme.of(context).colorScheme.primary; - return MouseRegion( - onEnter: (_) => setState(() => _isHovered = true), - onExit: (_) => setState(() => _isHovered = false), - child: GestureDetector( + return HoverBuilder( + builder: (context, hovered) => GestureDetector( onTap: widget.onSelectEmoji, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Stack( - children: [ - CompositedTransformTarget( - link: layerLink, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: _isHovered - ? Theme.of(context).colorScheme.primary.withAlpha(50) - : Colors.transparent, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - border: widget.isDisplay - ? Border.all( - color: AppConfig.goldLight, - width: 4, - ) - : null, - ), - child: Text( - widget.emoji, - style: Theme.of(context).textTheme.headlineSmall, - ), + child: Stack( + children: [ + CompositedTransformTarget( + link: layerLink, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: hovered + ? Theme.of(context).colorScheme.primary.withAlpha(50) + : Colors.transparent, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + border: widget.selected + ? Border.all( + color: AppConfig.goldLight.withAlpha(200), + width: 2, + ) + : null, + ), + child: Text( + widget.emoji, + style: Theme.of(context).textTheme.headlineSmall, ), ), - if (widget.showShimmer) - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: Shimmer.fromColors( - baseColor: shimmerColor.withValues(alpha: 0.1), - highlightColor: shimmerColor.withValues(alpha: 0.6), - direction: ShimmerDirection.ltr, - child: Container( - decoration: BoxDecoration( - color: shimmerColor.withValues(alpha: 0.3), - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - ), + ), + if (shimmer) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: Shimmer.fromColors( + baseColor: shimmerColor.withValues(alpha: 0.1), + highlightColor: shimmerColor.withValues(alpha: 0.6), + direction: ShimmerDirection.ltr, + child: Container( + decoration: BoxDecoration( + color: shimmerColor.withValues(alpha: 0.3), + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), ), ), ), ), - ], - ), + ), + if (widget.badge != null) + Positioned( + right: 0, + bottom: 0, + child: widget.badge!, + ), + ], ), ), ); diff --git a/lib/pangea/lemmas/lemma_meaning_widget.dart b/lib/pangea/lemmas/lemma_meaning_widget.dart index 29edac24e..191d4a161 100644 --- a/lib/pangea/lemmas/lemma_meaning_widget.dart +++ b/lib/pangea/lemmas/lemma_meaning_widget.dart @@ -1,71 +1,74 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; class LemmaMeaningWidget extends StatelessWidget { - final LemmaMeaningBuilderState controller; - - final ConstructUses constructUse; + final ConstructIdentifier constructId; final TextStyle? style; final InlineSpan? leading; const LemmaMeaningWidget({ super.key, - required this.controller, - required this.constructUse, + required this.constructId, this.style, this.leading, }); @override Widget build(BuildContext context) { - if (controller.isLoading) { - return const TextLoadingShimmer(); - } + return LemmaMeaningBuilder( + langCode: MatrixState.pangeaController.userController.userL2!.langCode, + constructId: constructId, + builder: (context, controller) { + if (controller.isLoading) { + return const TextLoadingShimmer(); + } - if (controller.error != null) { - if (controller.error is UnsubscribedException) { - return ErrorIndicator( - message: L10n.of(context).subscribeToUnlockDefinitions, - style: style, - onTap: () { - MatrixState.pangeaController.subscriptionController - .showPaywall(context); - }, - ); - } - return ErrorIndicator( - message: L10n.of(context).errorFetchingDefinition, - style: style, - ); - } - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: RichText( - textAlign: leading == null ? TextAlign.center : TextAlign.start, - text: TextSpan( + if (controller.error != null) { + if (controller.error is UnsubscribedException) { + return ErrorIndicator( + message: L10n.of(context).subscribeToUnlockDefinitions, style: style, - children: [ - if (leading != null) leading!, - if (leading != null) - const WidgetSpan(child: SizedBox(width: 6.0)), - TextSpan( - text: controller.lemmaInfo?.meaning, + onTap: () { + MatrixState.pangeaController.subscriptionController + .showPaywall(context); + }, + ); + } + return ErrorIndicator( + message: L10n.of(context).errorFetchingDefinition, + style: style, + ); + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: RichText( + textAlign: leading == null ? TextAlign.center : TextAlign.start, + text: TextSpan( + style: style, + children: [ + if (leading != null) leading!, + if (leading != null) + const WidgetSpan(child: SizedBox(width: 6.0)), + TextSpan( + text: controller.lemmaInfo?.meaning, + ), + ], ), - ], + ), ), - ), - ), - ], + ], + ); + }, ); } } diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart index 3e210bed1..79b2ab38f 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart @@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart'; import 'package:fluffychat/pangea/common/utils/async_state.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/speech_to_text/speech_to_text_response_model.dart'; @@ -192,26 +191,11 @@ class SelectModeController with LemmaEmojiSetter { selectedMode.value = mode; } - Future setTokenEmoji( + void setTokenEmoji( ConstructIdentifier constructId, String emoji, - String targetId, - ) async { - constructEmojiNotifier.value = (constructId, emoji); - try { - await setLemmaEmoji( - constructId, - emoji, - targetId, - ); - } catch (e, s) { - ErrorHandler.logError( - data: constructId.toJson(), - e: e, - s: s, - ); - } - } + ) => + constructEmojiNotifier.value = (constructId, emoji); Future fetchAudio() => _audioLoader.load(); Future fetchTranslation() => _translationLoader.load(); diff --git a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart index e2892da8d..4fe4c1c56 100644 --- a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart +++ b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart @@ -3,68 +3,107 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:matrix/matrix.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/constructs/construct_identifier.dart'; -import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; -import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart'; +import 'package:fluffychat/widgets/matrix.dart'; -class LemmaReactionPicker extends StatelessWidget { +class LemmaReactionPicker extends StatefulWidget { final Event? event; - final ConstructIdentifier construct; - final Future Function(String)? setEmoji; + final ConstructIdentifier constructId; + final Function(String)? onSetEmoji; final String langCode; const LemmaReactionPicker({ super.key, - required this.construct, - required this.setEmoji, + required this.constructId, + required this.onSetEmoji, required this.langCode, this.event, }); - Future onSelect( - String emoji, - List emojis, - ) async { - if (event?.room.timeline == null) { - throw Exception("Timeline is null in reaction picker"); - } + @override + State createState() => LemmaReactionPickerState(); +} - final client = event!.room.client; - final userSentEmojis = event! +class LemmaReactionPickerState extends State + with LemmaEmojiSetter { + String? _selectedEmoji; + + @override + void initState() { + super.initState(); + _setInitialEmoji(); + } + + @override + void didUpdateWidget(covariant LemmaReactionPicker oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.constructId != widget.constructId) { + _setInitialEmoji(); + } + } + + void _setInitialEmoji() { + setState( + () { + _selectedEmoji = widget.constructId.userLemmaInfo.emojis?.firstOrNull; + }, + ); + } + + Event? _sentReaction(String emoji) { + final userSentEmojis = widget.event! .aggregatedEvents( - event!.room.timeline!, + widget.event!.room.timeline!, RelationshipTypes.reaction, ) .where( - (e) => - e.senderId == client.userID && - emojis.contains(e.content.tryGetMap('m.relates_to')?['key']), + (e) => e.senderId == Matrix.of(context).client.userID, ); - final reactionEvent = userSentEmojis.firstWhereOrNull( + return userSentEmojis.firstWhereOrNull( (e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji, ); + } + + Future _setEmoji(String emoji) async { + setState(() => _selectedEmoji = emoji); + widget.onSetEmoji?.call(emoji); + + await setLemmaEmoji( + widget.constructId, + emoji, + "emoji-choice-item-$emoji-${widget.constructId.lemma}", + ); + + if (mounted) { + showLemmaEmojiSnackbar(context, widget.constructId, emoji); + } + } + + Future _sendOrRedactReaction(String emoji) async { + if (widget.event?.room.timeline == null) return; try { - await setEmoji?.call(emoji); - + final reactionEvent = _sentReaction(emoji); if (reactionEvent != null) { await reactionEvent.redactEvent(); return; } - await Future.wait([ - ...userSentEmojis.map((e) => e.redactEvent()), - event!.room.sendReaction(event!.eventId, emoji), - ]); + await widget.event!.room.sendReaction( + widget.event!.eventId, + emoji, + ); } catch (e, s) { ErrorHandler.logError( e: e, s: s, data: { 'emoji': emoji, - 'eventId': event?.eventId, + 'eventId': widget.event?.eventId, }, ); } @@ -72,44 +111,28 @@ class LemmaReactionPicker extends StatelessWidget { @override Widget build(BuildContext context) { - return LemmaMeaningBuilder( - langCode: langCode, - constructId: construct, - builder: (context, controller) { - final sentReactions = {}; - if (event?.room.timeline != null) { - sentReactions.addAll( - event! - .aggregatedEvents( - event!.room.timeline!, - RelationshipTypes.reaction, - ) - .where( - (event) => - event.senderId == event.room.client.userID && - event.type == 'm.reaction', - ) - .map( - (event) => event.content - .tryGetMap('m.relates_to') - ?.tryGet('key'), - ) - .whereType(), - ); - } - - return LemmaEmojiPicker( - emojis: controller.lemmaInfo?.emoji ?? [], - onSelect: event?.room.timeline != null - ? (emoji) => onSelect( - emoji, - controller.lemmaInfo?.emoji ?? [], - ) - : null, - disabled: (emoji) => sentReactions.contains(emoji), - loading: controller.isLoading, - ); - }, + return LemmaHighlightEmojiRow( + cId: widget.constructId, + langCode: widget.langCode, + onEmojiSelected: (emoji) => emoji != _selectedEmoji + ? _setEmoji(emoji) + : _sendOrRedactReaction(emoji), + emoji: _selectedEmoji, + selectedEmojiBadge: widget.event != null && + _selectedEmoji != null && + _sentReaction(_selectedEmoji!) == null + ? CircleAvatar( + backgroundColor: Theme.of(context).colorScheme.onSurface, + radius: 8.0, + child: Center( + child: Icon( + Icons.send, + size: 12.0, + color: Theme.of(context).colorScheme.surface, + ), + ), + ) + : null, ); } } diff --git a/lib/pangea/toolbar/word_card/reading_assistance_content.dart b/lib/pangea/toolbar/word_card/reading_assistance_content.dart index 486a92239..83dca55fb 100644 --- a/lib/pangea/toolbar/word_card/reading_assistance_content.dart +++ b/lib/pangea/toolbar/word_card/reading_assistance_content.dart @@ -54,7 +54,6 @@ class ReadingAssistanceContent extends StatelessWidget { setEmoji: (emoji) => overlayController.selectModeController.setTokenEmoji( overlayController.selectedToken!.vocabConstructID, emoji, - overlayController.tokenEmojiPopupKey(overlayController.selectedToken!), ), onFlagTokenInfo: (LemmaInfoResponse lemmaInfo, String phonetics) { if (selectedTokenIndex < 0) return; diff --git a/lib/pangea/toolbar/word_card/word_zoom_widget.dart b/lib/pangea/toolbar/word_card/word_zoom_widget.dart index 2da2e272d..caabccaa5 100644 --- a/lib/pangea/toolbar/word_card/word_zoom_widget.dart +++ b/lib/pangea/toolbar/word_card/word_zoom_widget.dart @@ -29,7 +29,7 @@ class WordZoomWidget extends StatelessWidget { final VoidCallback? onDismissNewWordOverlay; final Function(LemmaInfoResponse, String)? onFlagTokenInfo; - final Future Function(String)? setEmoji; + final Function(String)? setEmoji; const WordZoomWidget({ super.key, @@ -143,10 +143,10 @@ class WordZoomWidget extends StatelessWidget { iconSize: 24.0, ), LemmaReactionPicker( - construct: construct, + constructId: construct, langCode: langCode, + onSetEmoji: setEmoji, event: event, - setEmoji: setEmoji, ), LemmaMeaningDisplay( langCode: langCode,