From b7a6ee6fe2aa728ef6e7355622efbf30df237693 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:51:42 -0400 Subject: [PATCH] refactor: separate token and message reading assistance modes (#2416) * refactor: separate token and message reading assistance modes * chore: apply same token styling to HTML formatted messages * chore: don't wait for lemma responses before showing reading assistance content --- lib/config/app_config.dart | 2 +- lib/pages/chat/events/html_message.dart | 136 +++++-- lib/pages/chat/events/message_content.dart | 8 +- lib/pages/chat/reactions_picker.dart | 8 +- lib/pangea/common/utils/any_state_holder.dart | 4 + lib/pangea/common/widgets/customized_svg.dart | 10 +- .../events/utils/message_text_util.dart | 26 +- lib/pangea/morphs/morph_features_enum.dart | 20 + .../enums/reading_assistance_mode_enum.dart | 10 + .../overlay_footer.dart | 4 + .../reading_assistance_input_bar.dart | 10 +- .../toolbar/utils/token_rendering_util.dart | 93 +++++ .../widgets/message_selection_overlay.dart | 68 ++-- .../widgets/message_selection_positioner.dart | 375 +++++++++++------- .../toolbar/widgets/message_token_text.dart | 342 ++++++++-------- .../widgets/overlay_center_content.dart | 99 ++--- .../toolbar/widgets/overlay_message.dart | 4 + 17 files changed, 766 insertions(+), 453 deletions(-) create mode 100644 lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart create mode 100644 lib/pangea/toolbar/utils/token_rendering_util.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 1012b9e63..d9902f763 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -27,10 +27,10 @@ abstract class AppConfig { static const double toolbarMinHeight = 200.0; static const double toolbarMinWidth = 350.0; static const double defaultHeaderHeight = 56.0; - static const double readingAssistanceInputBarHeight = 170; static const double toolbarButtonsHeight = 50.0; static const double toolbarSpacing = 8.0; static const double toolbarIconSize = 24.0; + static const double readingAssistanceInputBarHeight = 140.0; static TextStyle messageTextStyle( Event? event, diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index f2c43209e..d484a6093 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -10,10 +10,16 @@ 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'; 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/practice_activities/activity_type_enum.dart'; +import 'package:fluffychat/pangea/message_token_text/message_token_button.dart'; +import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import '../../../utils/url_launcher.dart'; @@ -25,12 +31,14 @@ class HtmlMessage extends StatelessWidget { final TextStyle linkStyle; final void Function(LinkableElement) onOpen; // #Pangea - final bool isOverlay; + final MessageOverlayController? overlayController; final PangeaMessageEvent? pangeaMessageEvent; final ChatController controller; final Event event; final Event? nextEvent; final Event? prevEvent; + final bool isTransitionAnimation; + final ReadingAssistanceMode? readingAssistanceMode; final bool Function(PangeaToken)? isSelected; final void Function(PangeaToken)? onClick; @@ -45,7 +53,7 @@ class HtmlMessage extends StatelessWidget { this.textColor = Colors.black, required this.onOpen, // #Pangea - required this.isOverlay, + this.overlayController, required this.event, this.pangeaMessageEvent, required this.controller, @@ -53,6 +61,8 @@ class HtmlMessage extends StatelessWidget { this.prevEvent, this.isSelected, this.onClick, + this.isTransitionAnimation = false, + this.readingAssistanceMode, // Pangea# }); @@ -262,53 +272,91 @@ class HtmlMessage extends StatelessWidget { ? isSelected!.call(token) : false; - final shouldDo = pangeaMessageEvent?.shouldDoActivity( - token: token, - a: ActivityTypeEnum.wordMeaning, - feature: null, - tag: null, - ) ?? - false; + final renderer = TokenRenderingUtil( + pangeaMessageEvent: pangeaMessageEvent, + readingAssistanceMode: readingAssistanceMode, + existingStyle: AppConfig.messageTextStyle( + pangeaMessageEvent!.event, + textColor, + ), + overlayController: overlayController, + isTransitionAnimation: isTransitionAnimation, + ); - // @ggurdin: probably changing this, not sure when it shows up - final didMeaningActivity = token?.didActivitySuccessfully( - ActivityTypeEnum.wordMeaning, - ) ?? - true; - - Color backgroundColor = Colors.transparent; - if (selected) { - backgroundColor = Theme.of(context).colorScheme.primary.withAlpha(80); - } else if (isSelected != null && shouldDo) { - backgroundColor = !didMeaningActivity - ? AppConfig.success.withAlpha(60) - : AppConfig.gold.withAlpha(60); - } + final tokenWidth = renderer.tokenTextWidthForContainer( + context, + node.innerHtml, + ); return WidgetSpan( alignment: PlaceholderAlignment.middle, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onClick != null && token != null - ? () => onClick?.call(token) + child: CompositedTransformTarget( + link: token != null && renderer.assignTokenKey + ? MatrixState.pAnyState + .layerLinkAndKey(token.text.uniqueKey) + .link + : LayerLinkAndKey(token.hashCode.toString()).link, + child: Column( + key: token != null && renderer.assignTokenKey + ? MatrixState.pAnyState + .layerLinkAndKey(token.text.uniqueKey) + .key : null, - child: Text.rich( - TextSpan( - children: [ - LinkifySpan( - text: node.innerHtml, - style: AppConfig.messageTextStyle( - pangeaMessageEvent!.event, - textColor, - ).merge(TextStyle(backgroundColor: backgroundColor)), - linkStyle: linkStyle, - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), + children: [ + if (renderer.showCenterStyling && token != null) + MessageTokenButton( + token: token, + overlayController: overlayController, + textStyle: renderer.style( + context, + color: renderer.backgroundColor( + context, + selected, + ), ), - ], + width: tokenWidth, + animate: isTransitionAnimation, + practiceTarget: + overlayController?.toolbarMode.associatedActivityType != + null + ? overlayController?.practiceSelection + ?.activities( + overlayController! + .toolbarMode.associatedActivityType!, + ) + .firstWhereOrNull( + (a) => a.tokens.contains(token), + ) + : null, + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onClick != null && token != null + ? () => onClick?.call(token) + : null, + child: Text.rich( + TextSpan( + children: [ + LinkifySpan( + text: node.innerHtml, + style: renderer.style( + context, + color: renderer.backgroundColor( + context, + selected, + ), + ), + linkStyle: linkStyle, + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), + ), + ], + ), + ), + ), ), - ), + ], ), ), ); @@ -611,7 +659,7 @@ class HtmlMessage extends StatelessWidget { return SelectionArea( child: GestureDetector( onTap: () { - if (!isOverlay) { + if (overlayController == null) { controller.showToolbar( pangeaMessageEvent?.event ?? event, pangeaMessageEvent: pangeaMessageEvent, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index be6888e33..d9ff2a82d 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_selection_area.dart'; @@ -44,6 +45,7 @@ class MessageContent extends StatelessWidget { final Event? nextEvent; final Event? prevEvent; final bool isTransitionAnimation; + final ReadingAssistanceMode? readingAssistanceMode; // Pangea# final Timeline timeline; @@ -61,6 +63,7 @@ class MessageContent extends StatelessWidget { this.nextEvent, this.prevEvent, this.isTransitionAnimation = false, + this.readingAssistanceMode, // Pangea# required this.linkColor, required this.borderRadius, @@ -256,13 +259,15 @@ class MessageContent extends StatelessWidget { room: event.room, // #Pangea event: event, - isOverlay: overlayController != null, + overlayController: overlayController, controller: controller, pangeaMessageEvent: pangeaMessageEvent, nextEvent: nextEvent, prevEvent: prevEvent, isSelected: overlayController != null ? isSelected : null, onClick: event.isActivityMessage ? null : onClick, + isTransitionAnimation: isTransitionAnimation, + readingAssistanceMode: readingAssistanceMode, // Pangea# fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, linkStyle: TextStyle( @@ -394,6 +399,7 @@ class MessageContent extends StatelessWidget { true, overlayController: overlayController, isTransitionAnimation: isTransitionAnimation, + readingAssistanceMode: readingAssistanceMode, ); } diff --git a/lib/pages/chat/reactions_picker.dart b/lib/pages/chat/reactions_picker.dart index 2610a0d50..d2598abfc 100644 --- a/lib/pages/chat/reactions_picker.dart +++ b/lib/pages/chat/reactions_picker.dart @@ -53,9 +53,11 @@ class ReactionsPicker extends StatelessWidget { children: [ Expanded( child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.onInverseSurface, - borderRadius: const BorderRadius.only( + decoration: const BoxDecoration( + // #Pangea + // color: theme.colorScheme.onInverseSurface, + // Pangea# + borderRadius: BorderRadius.only( bottomRight: Radius.circular(AppConfig.borderRadius), ), ), diff --git a/lib/pangea/common/utils/any_state_holder.dart b/lib/pangea/common/utils/any_state_holder.dart index 0665cef4d..2cc03e67d 100644 --- a/lib/pangea/common/utils/any_state_holder.dart +++ b/lib/pangea/common/utils/any_state_holder.dart @@ -103,6 +103,10 @@ class PangeaAnyState { RenderBox? getRenderBox(String key) => layerLinkAndKey(key).key.currentContext?.findRenderObject() as RenderBox?; + + bool isOverlayOpen(String overlayKey) { + return entries.any((element) => element.key == overlayKey); + } } class LayerLinkAndKey { diff --git a/lib/pangea/common/widgets/customized_svg.dart b/lib/pangea/common/widgets/customized_svg.dart index 05f03c86e..aeb355de9 100644 --- a/lib/pangea/common/widgets/customized_svg.dart +++ b/lib/pangea/common/widgets/customized_svg.dart @@ -95,10 +95,12 @@ class _CustomizedSvgState extends State { _hasError = modifiedSvg == null; }); } catch (_) { - setState(() { - _isLoading = false; - _hasError = true; - }); + if (mounted) { + setState(() { + _isLoading = false; + _hasError = true; + }); + } } } diff --git a/lib/pangea/events/utils/message_text_util.dart b/lib/pangea/events/utils/message_text_util.dart index a29196ab2..c306d6715 100644 --- a/lib/pangea/events/utils/message_text_util.dart +++ b/lib/pangea/events/utils/message_text_util.dart @@ -18,10 +18,10 @@ class TokenPosition { /// End index of the token in the message final int tokenEnd; - final bool selected; final bool hideContent; final PangeaToken? token; final bool isHighlighted; + final bool selected; const TokenPosition({ required this.start, @@ -36,6 +36,8 @@ class TokenPosition { } class MessageTextUtil { + static final Map> _tokenPositionsCache = {}; + static List? getTokenPositions( PangeaMessageEvent pangeaMessageEvent, { PracticeSelection? messageAnalyticsEntry, @@ -47,6 +49,27 @@ class MessageTextUtil { return null; } + if (_tokenPositionsCache.containsKey(pangeaMessageEvent.eventId)) { + return _tokenPositionsCache[pangeaMessageEvent.eventId]! + .map( + (t) => TokenPosition( + start: t.start, + end: t.end, + tokenStart: t.tokenStart, + tokenEnd: t.tokenEnd, + hideContent: t.hideContent, + selected: t.token != null + ? isSelected?.call(t.token!) ?? false + : false, + isHighlighted: t.token != null + ? isHighlighted?.call(t.token!) ?? false + : false, + token: t.token, + ), + ) + .toList(); + } + // Convert the entire message into a list of characters final Characters messageCharacters = pangeaMessageEvent.messageDisplayText.characters; @@ -131,6 +154,7 @@ class MessageTextUtil { continue; } + _tokenPositionsCache[pangeaMessageEvent.eventId] = tokenPositions; return tokenPositions; } catch (err, s) { ErrorHandler.logError( diff --git a/lib/pangea/morphs/morph_features_enum.dart b/lib/pangea/morphs/morph_features_enum.dart index 495797b09..aed860298 100644 --- a/lib/pangea/morphs/morph_features_enum.dart +++ b/lib/pangea/morphs/morph_features_enum.dart @@ -41,6 +41,18 @@ enum MorphFeaturesEnum { Unknown, } +class MorphFeatureUtil { + static final Map _morphFeatureCache = {}; + + static void set(String key, MorphFeaturesEnum value) { + _morphFeatureCache[key] = value; + } + + static MorphFeaturesEnum? get(String key) { + return _morphFeatureCache[key]; + } +} + extension MorphFeaturesEnumExtension on MorphFeaturesEnum { /// Convert enum to string String toShortString() { @@ -49,6 +61,12 @@ extension MorphFeaturesEnumExtension on MorphFeaturesEnum { /// Convert string to enum static MorphFeaturesEnum fromString(String category) { + // Repeated regex operations are causing performance issues, + // so we cache the results in a static map + if (MorphFeatureUtil.get(category) != null) { + return MorphFeatureUtil.get(category)!; + } + final morph = MorphFeaturesEnum.values.firstWhereOrNull( (e) => e.toShortString() == @@ -62,6 +80,8 @@ extension MorphFeaturesEnumExtension on MorphFeaturesEnum { ); return MorphFeaturesEnum.Unknown; } + + MorphFeatureUtil.set(category, morph); return morph; } diff --git a/lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart b/lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart new file mode 100644 index 000000000..f0ef61617 --- /dev/null +++ b/lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart @@ -0,0 +1,10 @@ +enum ReadingAssistanceMode { + /// Overlay message is directly over the original message + tokenMode, + + /// Overlay message is centered and larger than the original message + messageMode, + + /// Overlay message is moving to the center of the screen + transitionMode, +} diff --git a/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart b/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart index 1daeb39d9..1ea455a6c 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart @@ -33,8 +33,12 @@ class OverlayFooter extends StatelessWidget { // constraints: const BoxConstraints( // maxWidth: FluffyThemes.columnWidth * 2.5, // ), + height: AppConfig.readingAssistanceInputBarHeight + + AppConfig.toolbarButtonsHeight + + 20.0, alignment: Alignment.center, child: Column( + mainAxisAlignment: MainAxisAlignment.end, children: [ if (showToolbarButtons) ToolbarButtonRow(overlayController: overlayController), diff --git a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart index e008f66cd..a41218570 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart @@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; @@ -48,7 +49,7 @@ class ReadingAssistanceInputBar extends StatelessWidget { case MessageMode.noneSelected: case MessageMode.messageMeaning: //TODO: show all emojis for the lemmas and allow sending normal reactions - break; + return ReactionsPicker(controller); case MessageMode.messageTranslation: if (overlayController.isTranslationUnlocked) { @@ -97,10 +98,6 @@ class ReadingAssistanceInputBar extends StatelessWidget { } } - if (content == null) { - return const SizedBox(); - } - return Container( constraints: const BoxConstraints( minHeight: minContentHeight, @@ -114,8 +111,7 @@ class ReadingAssistanceInputBar extends StatelessWidget { return Expanded( child: ConstrainedBox( constraints: BoxConstraints( - maxHeight: (MediaQuery.of(context).size.height / 2) - - AppConfig.toolbarButtonsHeight, + maxHeight: AppConfig.readingAssistanceInputBarHeight, maxWidth: overlayController.maxWidth, ), child: AnimatedSize( diff --git a/lib/pangea/toolbar/utils/token_rendering_util.dart b/lib/pangea/toolbar/utils/token_rendering_util.dart new file mode 100644 index 000000000..1ce3f8109 --- /dev/null +++ b/lib/pangea/toolbar/utils/token_rendering_util.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.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'; + +class TokenRenderingUtil { + final PangeaMessageEvent? pangeaMessageEvent; + final ReadingAssistanceMode? readingAssistanceMode; + final MessageOverlayController? overlayController; + final bool isTransitionAnimation; + final TextStyle existingStyle; + + static final Map _tokensWidthCache = {}; + + TokenRenderingUtil({ + required this.pangeaMessageEvent, + required this.readingAssistanceMode, + required this.existingStyle, + this.overlayController, + this.isTransitionAnimation = false, + }); + + bool get showCenterStyling { + if (overlayController == null) return false; + if (!isTransitionAnimation) return true; + return readingAssistanceMode == ReadingAssistanceMode.transitionMode; + } + + double? _fontSize(BuildContext context) => showCenterStyling + ? overlayController != null && overlayController!.maxWidth > 600 + ? Theme.of(context).textTheme.titleLarge?.fontSize + : Theme.of(context).textTheme.bodyLarge?.fontSize + : null; + + TextStyle style( + BuildContext context, { + Color? color, + }) => + existingStyle.copyWith( + fontSize: _fontSize(context), + decoration: TextDecoration.underline, + decorationThickness: 4, + decorationColor: color ?? Colors.white.withAlpha(0), + ); + + double tokenTextWidthForContainer(BuildContext context, String text) { + final tokenSizeKey = "$text-${_fontSize(context)}"; + if (_tokensWidthCache.containsKey(tokenSizeKey)) { + return _tokensWidthCache[tokenSizeKey]!; + } + + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: style(context), + ), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(); + final width = textPainter.width; + textPainter.dispose(); + + _tokensWidthCache[tokenSizeKey] = width; + return width; + } + + // Only one token on the screen can have the token's unique key at a time. + // When readingAssistanceMode is not null, there are two messages - the centered message and the transition message. + // When in word mode, the key goes to the transition message. + // If actively transitioning, neither gets the keys. + // If in message mode, the key goes to the centered message (isTransitionAnimation == false). + bool get assignTokenKey { + if (readingAssistanceMode == null) { + return false; + } + + switch (readingAssistanceMode!) { + case ReadingAssistanceMode.tokenMode: + return isTransitionAnimation; + case ReadingAssistanceMode.transitionMode: + return false; + case ReadingAssistanceMode.messageMode: + return !isTransitionAnimation; + } + } + + Color backgroundColor(BuildContext context, bool selected) { + 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 216330e11..7c45ce29f 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -142,16 +142,22 @@ class MessageOverlayController extends State .messageDisplayRepresentation!.tokensToSave .map((e) => e.vocabConstructID) .toList(); + final List> lemmaInfoFutures = messageVocabConstructIds .map((token) => token.getLemmaInfo()) .toList(); - final List lemmaInfos = - await Future.wait(lemmaInfoFutures); - messageLemmaInfos = Map.fromIterables( - messageVocabConstructIds, - lemmaInfos, - ); + + Future.wait(lemmaInfoFutures).then((resp) { + if (mounted) { + setState( + () => messageLemmaInfos = Map.fromIterables( + messageVocabConstructIds, + resp, + ), + ); + } + }); } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError( @@ -285,28 +291,38 @@ class MessageOverlayController extends State _selectedSpan = selectedSpan; } - if (selectedToken != null) { - final entry = ReadingAssistanceContent( - key: wordZoomKey, - pangeaMessageEvent: pangeaMessageEvent!, - overlayController: this, - ); - if (mounted) { - OverlayUtil.showPositionedCard( - context: context, - cardToShow: entry, - transformTargetId: selectedToken!.text.uniqueKey, - closePrevOverlay: false, - backDropToDismiss: false, - addBorder: false, - overlayKey: selectedToken!.text.uniqueKey, - maxHeight: AppConfig.toolbarMaxHeight, - maxWidth: AppConfig.toolbarMinWidth, - ); - } + if (mounted) setState(() {}); + Future.delayed(const Duration(milliseconds: 10), () { + _showReadingAssistanceContent(); + }); + } + + void _showReadingAssistanceContent() { + if (selectedToken == null) return; + if (MatrixState.pAnyState.isOverlayOpen( + selectedToken!.text.uniqueKey, + )) { + return; } - if (mounted) setState(() {}); + final entry = ReadingAssistanceContent( + key: wordZoomKey, + pangeaMessageEvent: pangeaMessageEvent!, + overlayController: this, + ); + if (mounted) { + OverlayUtil.showPositionedCard( + context: context, + cardToShow: entry, + transformTargetId: selectedToken!.text.uniqueKey, + closePrevOverlay: false, + backDropToDismiss: false, + addBorder: false, + overlayKey: selectedToken!.text.uniqueKey, + maxHeight: AppConfig.toolbarMaxHeight, + maxWidth: AppConfig.toolbarMinWidth, + ); + } } void updateToolbarMode(MessageMode mode) => setState(() { diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart index 54059e86f..9daf90d8e 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart'; import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; @@ -53,31 +54,36 @@ class MessageSelectionPositioner extends StatefulWidget { class MessageSelectionPositionerState extends State with TickerProviderStateMixin { late AnimationController _animationController; - Animation? _overlayOffsetAnimation; - Animation? _messageSizeAnimation; - - StreamSubscription? _reactionSubscription; Offset? _centeredMessageOffset; Size? _centeredMessageSize; Size? _tooltipSize; - Size? _inputBarSize; final Completer _centeredMessageCompleter = Completer(); final Completer _tooltipCompleter = Completer(); - bool _finishedAnimation = false; + MessageMode _currentMode = MessageMode.noneSelected; + ReadingAssistanceMode? _readingAssistanceMode; + + Animation? _overlayOffsetAnimation; + Animation? _messageSizeAnimation; + Offset? _currentOffset; + + StreamSubscription? _reactionSubscription; + + final _animationDuration = const Duration( + milliseconds: AppConfig.overlayAnimationDuration, + // seconds: 5, + ); @override void initState() { super.initState(); + _currentMode = widget.overlayController.toolbarMode; _animationController = AnimationController( vsync: this, - duration: const Duration( - milliseconds: AppConfig.overlayAnimationDuration, - // seconds: 5, - ), + duration: _animationDuration, ); _reactionSubscription = @@ -100,26 +106,37 @@ class MessageSelectionPositionerState extends State }, ).listen((_) => setState(() {})); - Future.wait([ - _centeredMessageCompleter.future, - if (showToolbarButtons) _tooltipCompleter.future, - ]).then((_) => _startAnimation()); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _centeredMessageCompleter.future; + if (!mounted) return; + + setState(() { + _currentOffset = Offset( + _ownMessage ? _messageRightOffset : _messageLeftOffset, + _originalMessageBottomOffset - _reactionsHeight, + ); + }); + + _setReadingAssistanceMode( + widget.initialSelectedToken == null + ? ReadingAssistanceMode.messageMode + : ReadingAssistanceMode.tokenMode, + ); + }); } @override void didUpdateWidget(MessageSelectionPositioner oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.overlayController.toolbarMode != - widget.overlayController.toolbarMode) { - setState(() {}); + final mode = widget.overlayController.toolbarMode; + if (mode != _currentMode) { + if (_currentMode == MessageMode.noneSelected) { + _setReadingAssistanceMode(ReadingAssistanceMode.messageMode); + } + setState(() => _currentMode = mode); } } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - } - @override void dispose() { _animationController.dispose(); @@ -154,46 +171,68 @@ class MessageSelectionPositionerState extends State } } - void _setInputBarSize(RenderBox renderBox) { - setState(() => _inputBarSize = renderBox.size); - } - - void _startAnimation() { - if (_mediaQuery == null) { + Future _setReadingAssistanceMode(ReadingAssistanceMode mode) async { + if (mode == _readingAssistanceMode) { return; } - _overlayOffsetAnimation = Tween( - begin: Offset( - _ownMessage ? _messageRightOffset : _messageLeftOffset, - _messageBottomOffset - _reactionsHeight, - ), - // For own messages, dx is the right offset. For other's messages, dx is the left offset. - end: _adjustedCenteredMessageOffset, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: FluffyThemes.animationCurve, - ), - ); + await _centeredMessageCompleter.future; - _messageSizeAnimation = Tween( - begin: Size( - _messageSize.width, - _originalMessageHeight, - ), - end: _adjustedCenteredMessageSize, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: FluffyThemes.animationCurve, - ), - ); + if (mode == ReadingAssistanceMode.messageMode) { + setState( + () => _readingAssistanceMode = ReadingAssistanceMode.transitionMode, + ); + } else if (mode == ReadingAssistanceMode.tokenMode) { + setState( + () => _readingAssistanceMode = ReadingAssistanceMode.tokenMode, + ); + } - _animationController.forward().then((_) { - _finishedAnimation = true; - if (mounted) setState(() {}); - }); + if (mode == ReadingAssistanceMode.tokenMode) { + _overlayOffsetAnimation = Tween( + begin: _currentOffset, + end: _adjustedOriginalMessageOffset, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: FluffyThemes.animationCurve, + ), + )..addListener(() { + if (mounted) { + setState(() => _currentOffset = _overlayOffsetAnimation?.value); + } + }); + } else if (mode == ReadingAssistanceMode.messageMode) { + _overlayOffsetAnimation = Tween( + begin: _currentOffset, + end: _centeredMessageOffset!, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: FluffyThemes.animationCurve, + ), + )..addListener(() { + if (mounted) { + setState(() => _currentOffset = _overlayOffsetAnimation?.value); + } + }); + + _messageSizeAnimation = Tween( + begin: Size( + _originalMessageSize.width, + _originalMessageSize.height, + ), + end: _adjustedCenteredMessageSize, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: FluffyThemes.animationCurve, + ), + ); + } + + await _animationController.forward(from: 0); + if (mounted) setState(() => _readingAssistanceMode = mode); } T _runWithLogging( @@ -215,6 +254,10 @@ class MessageSelectionPositionerState extends State } } + final double _inputBarSize = AppConfig.readingAssistanceInputBarHeight + + AppConfig.toolbarButtonsHeight + + 20.0; + bool get _showDetails => (Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ?? false) && @@ -233,70 +276,12 @@ class MessageSelectionPositionerState extends State ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth) : 0; - // message size - - RenderBox? get _messageRenderBox => _runWithLogging( - () => MatrixState.pAnyState.getRenderBox( - widget.event.eventId, - ), - "Error getting message render box", - null, - ); - - Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100); - Size get _messageSize { - if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { - return _defaultMessageSize; - } - - return _runWithLogging( - () => _messageRenderBox?.size, - "Error getting message size", - _defaultMessageSize, - ); - } - - double get _originalMessageHeight => _messageSize.height; - - double? get _centerSpace { + /// Available vertical space not taken up by the header and footer + double? get _verticalSpace { if (_mediaQuery == null) return null; return _mediaQuery!.size.height - _headerHeight - _footerHeight; } - bool get _centeredMessageHasOverflow { - if (_centerSpace == null || - _centeredMessageSize == null || - _centeredMessageOffset == null) { - return false; - } - - final finalMessageHeight = _centeredMessageSize!.height + _reactionsHeight; - return finalMessageHeight > _centerSpace!; - } - - Size? get _adjustedCenteredMessageSize { - if (_centeredMessageHasOverflow) { - return Size( - _centeredMessageSize!.width, - _centerSpace! - (AppConfig.toolbarSpacing * 2), - ); - } - return _centeredMessageSize; - } - - Offset? get _adjustedCenteredMessageOffset { - if (_centeredMessageHasOverflow) { - return Offset( - _centeredMessageOffset!.dx, - _footerHeight + AppConfig.toolbarSpacing, - ); - } - return _centeredMessageOffset; - } - - //TODO: figure out where the 16 and 8 come from and use references instead of hard-coded values - static const _messageDefaultLeftMargin = Avatar.defaultSize + 16 + 8; - double get _toolbarMaxWidth { const double messageMargin = 16.0; // widget.event.isActivityMessage ? 0 : Avatar.defaultSize + 16 + 8; @@ -318,11 +303,73 @@ class MessageSelectionPositionerState extends State return maxWidth; } + // original message size and offset + + RenderBox? get _messageRenderBox => _runWithLogging( + () => MatrixState.pAnyState.getRenderBox( + widget.event.eventId, + ), + "Error getting message render box", + null, + ); + + Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100); + + /// The size of the message in the chat list (as opposed to the expanded size in the center overlay) + Size get _originalMessageSize { + if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { + return _defaultMessageSize; + } + + return _runWithLogging( + () => _messageRenderBox?.size, + "Error getting message size", + _defaultMessageSize, + ); + } + + static const _messageDefaultLeftMargin = Avatar.defaultSize + 16 + 8; + + // Centered message size and offset + + bool get _centeredMessageHasOverflow { + if (_verticalSpace == null || + _centeredMessageSize == null || + _centeredMessageOffset == null) { + return false; + } + + final finalMessageHeight = _centeredMessageSize!.height + _reactionsHeight; + return finalMessageHeight > _verticalSpace!; + } + + /// Size of the centered overlay message adjusted for overflow + Size? get _adjustedCenteredMessageSize { + if (_centeredMessageHasOverflow) { + return Size( + _centeredMessageSize!.width, + _verticalSpace! - (AppConfig.toolbarSpacing * 2), + ); + } + return _centeredMessageSize; + } + + Offset? get _adjustedCenteredMessageOffset { + if (_centeredMessageHasOverflow) { + return Offset( + _centeredMessageOffset!.dx, + _footerHeight + AppConfig.toolbarSpacing, + ); + } + return _centeredMessageOffset; + } + // message offset static const Offset _defaultMessageOffset = Offset(_messageDefaultLeftMargin, 300); - Offset get _messageOffset { + + Offset get _originalMessageOffset { if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { return _defaultMessageOffset; } @@ -333,8 +380,50 @@ class MessageSelectionPositionerState extends State ); } - double get _messageBottomOffset => - _mediaQuery!.size.height - _messageOffset.dy - _originalMessageHeight; + Offset get _adjustedOriginalMessageOffset { + if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { + return _defaultMessageOffset; + } + + final topOffset = _originalMessageOffset.dy; + final bottomOffset = _originalMessageBottomOffset; + final hasHeaderOverflow = + topOffset < (_headerHeight + AppConfig.toolbarSpacing); + final hasFooterOverflow = + bottomOffset < (_footerHeight + AppConfig.toolbarSpacing); + + if (!hasHeaderOverflow && !hasFooterOverflow) { + return Offset( + _ownMessage ? _messageRightOffset : _messageLeftOffset, + _originalMessageBottomOffset - _reactionsHeight, + ); + } + + if (hasHeaderOverflow) { + final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing); + return Offset( + _ownMessage ? _messageRightOffset : _messageLeftOffset, + _mediaQuery!.size.height - + _originalMessageOffset.dy + + difference - + _originalMessageSize.height, + ); + } else { + final difference = + bottomOffset - (_footerHeight + AppConfig.toolbarSpacing); + return Offset( + _ownMessage ? _messageRightOffset : _messageLeftOffset, + _mediaQuery!.size.height - + (_originalMessageOffset.dy + difference) - + _originalMessageSize.height, + ); + } + } + + double get _originalMessageBottomOffset => + _mediaQuery!.size.height - + _originalMessageOffset.dy - + _originalMessageSize.height; double? get _centeredMessageTopOffset { if (_mediaQuery == null || @@ -349,7 +438,7 @@ class MessageSelectionPositionerState extends State } double get _messageLeftOffset => max( - _messageOffset.dx - _columnWidth - _horizontalPadding, + _originalMessageOffset.dx - _columnWidth - _horizontalPadding, 0, ); @@ -358,8 +447,8 @@ class MessageSelectionPositionerState extends State return 0; } return _mediaQuery!.size.width - - _messageOffset.dx - - _messageSize.width - + _originalMessageOffset.dx - + _originalMessageSize.width - _horizontalPadding; } @@ -375,9 +464,7 @@ class MessageSelectionPositionerState extends State } double get _footerHeight { - return (_inputBarSize?.height ?? - (showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0)) + - (_mediaQuery?.padding.bottom ?? 0); + return _inputBarSize + (_mediaQuery?.padding.bottom ?? 0); } // measurement for items in the toolbar @@ -432,13 +519,14 @@ class MessageSelectionPositionerState extends State child: SizedBox.shrink(), ), Opacity( - opacity: _finishedAnimation ? 1.0 : 0.0, + opacity: + _readingAssistanceMode == ReadingAssistanceMode.messageMode + ? 1.0 + : 0.0, child: OverlayCenterContent( event: widget.event, messageHeight: null, messageWidth: null, - // messageHeight: _adjustedCenteredMessageSize?.height, - // messageWidth: _adjustedCenteredMessageSize?.width, maxWidth: widget.overlayController.maxWidth, overlayController: widget.overlayController, chatController: widget.chatController, @@ -448,11 +536,11 @@ class MessageSelectionPositionerState extends State hasReactions: _hasReactions, onChangeMessageSize: _setCenteredMessageSize, isTransitionAnimation: false, - transitionAnimationFinished: _finishedAnimation, maxHeight: _mediaQuery!.size.height - _headerHeight - _footerHeight - AppConfig.toolbarSpacing * 2, + readingAssistanceMode: _readingAssistanceMode, ), ), const Expanded( @@ -466,13 +554,10 @@ class MessageSelectionPositionerState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - MeasureRenderBox( - onChange: _setInputBarSize, - child: OverlayFooter( - controller: widget.chatController, - overlayController: widget.overlayController, - showToolbarButtons: showToolbarButtons, - ), + OverlayFooter( + controller: widget.chatController, + overlayController: widget.overlayController, + showToolbarButtons: showToolbarButtons, ), SizedBox(height: _mediaQuery?.padding.bottom ?? 0), ], @@ -486,7 +571,7 @@ class MessageSelectionPositionerState extends State ), ], ), - if (!_finishedAnimation) + if (_readingAssistanceMode != ReadingAssistanceMode.messageMode) AnimatedBuilder( animation: _overlayOffsetAnimation ?? _animationController, builder: (context, child) { @@ -500,11 +585,11 @@ class MessageSelectionPositionerState extends State _messageRightOffset : null, bottom: (_overlayOffsetAnimation?.value)?.dy ?? - _messageBottomOffset - _reactionsHeight, + _originalMessageBottomOffset - _reactionsHeight, child: OverlayCenterContent( event: widget.event, - messageHeight: _originalMessageHeight, - messageWidth: _messageSize.width, + messageHeight: _originalMessageSize.height, + messageWidth: _originalMessageSize.width, maxWidth: widget.overlayController.maxWidth, overlayController: widget.overlayController, chatController: widget.chatController, @@ -514,11 +599,11 @@ class MessageSelectionPositionerState extends State hasReactions: _hasReactions, sizeAnimation: _messageSizeAnimation, isTransitionAnimation: true, - transitionAnimationFinished: _finishedAnimation, maxHeight: _mediaQuery!.size.height - _headerHeight - _footerHeight - AppConfig.toolbarSpacing * 2, + readingAssistanceMode: _readingAssistanceMode, ), ); }, @@ -547,7 +632,11 @@ class MessageSelectionPositionerState extends State ), ), ), - if (_centeredMessageTopOffset != null && _tooltipSize != null) + if (_centeredMessageTopOffset != null && + _tooltipSize != null && + widget.overlayController.toolbarMode != + MessageMode.noneSelected && + widget.overlayController.selectedToken == null) Positioned( top: max( ((_headerHeight + _centeredMessageTopOffset!) / 2) - diff --git a/lib/pangea/toolbar/widgets/message_token_text.dart b/lib/pangea/toolbar/widgets/message_token_text.dart index 58e5e8e35..b4bd1d745 100644 --- a/lib/pangea/toolbar/widgets/message_token_text.dart +++ b/lib/pangea/toolbar/widgets/message_token_text.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/common/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; @@ -12,6 +11,8 @@ import 'package:fluffychat/pangea/message_token_text/message_token_button.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; +import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -27,9 +28,9 @@ class MessageTokenText extends StatelessWidget { final bool Function(PangeaToken)? _isSelected; final void Function(PangeaToken)? _onClick; final bool Function(PangeaToken)? _isHighlighted; - final MessageMode? _messageMode; final MessageOverlayController? _overlayController; final bool _isTransitionAnimation; + final ReadingAssistanceMode? readingAssistanceMode; const MessageTokenText({ super.key, @@ -42,11 +43,11 @@ class MessageTokenText extends StatelessWidget { MessageMode? messageMode, MessageOverlayController? overlayController, bool isTransitionAnimation = false, + this.readingAssistanceMode, }) : _onClick = onClick, _isSelected = isSelected, _style = style, _pangeaMessageEvent = pangeaMessageEvent, - _messageMode = messageMode, _isHighlighted = isHighlighted, _overlayController = overlayController, _isTransitionAnimation = isTransitionAnimation; @@ -82,55 +83,10 @@ class MessageTokenText extends StatelessWidget { messageAnalyticsEntry: messageAnalyticsEntry, isSelected: _isSelected, onClick: callOnClick, - messageMode: _messageMode, isHighlighted: _isHighlighted, overlayController: _overlayController, isTransitionAnimation: _isTransitionAnimation, - ); - } -} - -class HiddenText extends StatelessWidget { - final String text; - final TextStyle style; - - const HiddenText({ - super.key, - required this.text, - required this.style, - }); - - @override - Widget build(BuildContext context) { - final TextPainter textPainter = TextPainter( - text: TextSpan(text: text, style: style), - textDirection: TextDirection.ltr, - )..layout(); - - final textWidth = textPainter.size.width; - final textHeight = textPainter.size.height; - - textPainter.dispose(); - - return SizedBox( - height: textHeight, - child: Stack( - children: [ - Container( - width: textWidth, - height: textHeight, - color: Colors.transparent, - ), - Positioned( - bottom: 0, - child: Container( - width: textWidth, - height: 1, - color: style.color, - ), - ), - ], - ), + readingAssistanceMode: readingAssistanceMode, ); } } @@ -146,12 +102,11 @@ class MessageTextWidget extends StatelessWidget { final bool? softWrap; final int? maxLines; final TextOverflow? overflow; - final MessageMode? messageMode; - final Animation? contentSizeAnimation; final MessageOverlayController? overlayController; final bool isTransitionAnimation; final bool isMessage; + final ReadingAssistanceMode? readingAssistanceMode; const MessageTextWidget({ super.key, @@ -163,56 +118,23 @@ class MessageTextWidget extends StatelessWidget { this.softWrap, this.maxLines, this.overflow, - this.messageMode, this.isHighlighted, - this.contentSizeAnimation, this.overlayController, this.isTransitionAnimation = false, this.isMessage = true, + this.readingAssistanceMode, }); - TextStyle style(BuildContext context) => !isMessage - ? existingStyle - : overlayController != null && overlayController!.maxWidth > 600 - ? existingStyle.copyWith( - fontSize: Theme.of(context).textTheme.titleLarge?.fontSize, - ) - : existingStyle.copyWith( - fontSize: Theme.of(context).textTheme.bodyLarge?.fontSize, - ); - - /// for some reason, this isn't the same as tokenTextWidth - double tokenTextWidthForContainer(BuildContext context, PangeaToken token) { - final textPainter = TextPainter( - text: TextSpan(text: token.text.content, style: style(context)), - maxLines: 1, - textDirection: TextDirection.ltr, - )..layout(); - final width = textPainter.width; - textPainter.dispose(); - return width; - } - - Color backgroundColor(BuildContext context, TokenPosition tokenPosition) { - final hideTokenHighlights = messageAnalyticsEntry != null && - (messageAnalyticsEntry!.hasHiddenWordActivity || - messageAnalyticsEntry!.hasMessageMeaningActivity); - - Color backgroundColor = Colors.transparent; - - if (!hideTokenHighlights) { - if (tokenPosition.selected) { - backgroundColor = Theme.of(context).colorScheme.primary; - } - // else if (tokenPosition.isHighlighted) { - // backgroundColor = AppConfig.success.withAlpha(80); - // } - } - return backgroundColor; - } - @override Widget build(BuildContext context) { + final renderer = TokenRenderingUtil( + pangeaMessageEvent: pangeaMessageEvent, + readingAssistanceMode: readingAssistanceMode, + existingStyle: existingStyle, + overlayController: overlayController, + isTransitionAnimation: isTransitionAnimation, + ); + final Characters messageCharacters = pangeaMessageEvent.messageDisplayText.characters; @@ -226,7 +148,7 @@ class MessageTextWidget extends StatelessWidget { if (tokenPositions == null) { return Text( pangeaMessageEvent.messageDisplayText, - style: style(context), + style: renderer.style(context), softWrap: softWrap, maxLines: maxLines, overflow: overflow, @@ -278,39 +200,45 @@ class MessageTextWidget extends StatelessWidget { final token = tokenPosition.token!; - final tokenWidth = tokenTextWidthForContainer(context, token); + final tokenWidth = renderer.tokenTextWidthForContainer( + context, + token.text.content, + ); return WidgetSpan( child: CompositedTransformTarget( - link: overlayController == null || isTransitionAnimation - ? LayerLinkAndKey(token.hashCode.toString()).link - : MatrixState.pAnyState + link: renderer.assignTokenKey + ? MatrixState.pAnyState .layerLinkAndKey(token.text.uniqueKey) - .link, + .link + : LayerLinkAndKey(token.hashCode.toString()).link, child: Column( - key: overlayController == null || isTransitionAnimation - ? null - : MatrixState.pAnyState + key: renderer.assignTokenKey + ? MatrixState.pAnyState .layerLinkAndKey(token.text.uniqueKey) - .key, + .key + : null, children: [ - MessageTokenButton( - token: token, - overlayController: overlayController, - textStyle: style(context), - width: tokenWidth, - animate: isTransitionAnimation, - practiceTarget: overlayController - ?.toolbarMode.associatedActivityType != - null - ? overlayController?.practiceSelection - ?.activities( - overlayController! - .toolbarMode.associatedActivityType!, - ) - .firstWhereOrNull((a) => a.tokens.contains(token)) - : null, - ), + if (renderer.showCenterStyling) + MessageTokenButton( + token: token, + overlayController: overlayController, + textStyle: renderer.style(context), + width: tokenWidth, + animate: isTransitionAnimation, + practiceTarget: overlayController + ?.toolbarMode.associatedActivityType != + null + ? overlayController?.practiceSelection + ?.activities( + overlayController! + .toolbarMode.associatedActivityType!, + ) + .firstWhereOrNull( + (a) => a.tokens.contains(token), + ) + : null, + ), MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( @@ -324,7 +252,13 @@ class MessageTextWidget extends StatelessWidget { if (start.isNotEmpty) LinkifySpan( text: start, - style: style(context), + style: renderer.style( + context, + color: renderer.backgroundColor( + context, + tokenPosition.selected, + ), + ), linkStyle: TextStyle( decoration: TextDecoration.underline, color: linkColor, @@ -332,39 +266,46 @@ class MessageTextWidget extends StatelessWidget { onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ), - tokenPosition.hideContent - ? WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: GestureDetector( - onTap: onClick != null - ? () => onClick?.call(tokenPosition) - : null, - child: HiddenText( - text: middle, - style: style(context), - ), - ), - ) - : LinkifySpan( - text: middle, - // style: style.merge( - // TextStyle( - // backgroundColor: backgroundColor(tokenPosition) - // ), - // ), - style: style(context), - linkStyle: TextStyle( - decoration: TextDecoration.underline, - color: linkColor, - ), - onOpen: (url) => - UrlLauncher(context, url.url) - .launchUrl(), - ), + // tokenPosition.hideContent + // ? WidgetSpan( + // alignment: PlaceholderAlignment.middle, + // child: GestureDetector( + // onTap: onClick != null + // ? () => onClick?.call(tokenPosition) + // : null, + // child: HiddenText( + // text: middle, + // style: style(context), + // ), + // ), + // ) + // : + LinkifySpan( + text: middle, + style: renderer.style( + context, + color: renderer.backgroundColor( + context, + tokenPosition.selected, + ), + ), + linkStyle: TextStyle( + decoration: TextDecoration.underline, + color: linkColor, + ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), + ), if (end.isNotEmpty) LinkifySpan( text: end, - style: style(context), + style: renderer.style( + context, + color: renderer.backgroundColor( + context, + tokenPosition.selected, + ), + ), linkStyle: TextStyle( decoration: TextDecoration.underline, color: linkColor, @@ -377,39 +318,41 @@ class MessageTextWidget extends StatelessWidget { ), ), ), - AnimatedContainer( - duration: const Duration( - milliseconds: AppConfig.overlayAnimationDuration, - ), - height: - overlayController != null && !isTransitionAnimation - ? 4 - : 0, - width: tokenWidth, - child: Container( - color: backgroundColor(context, tokenPosition), - ), - ), + // AnimatedContainer( + // duration: const Duration( + // milliseconds: AppConfig.overlayAnimationDuration, + // ), + // height: overlayController != null && isTransitionAnimation + // ? 4 + // : 0, + // width: tokenWidth, + // child: Container( + // color: backgroundColor(context, tokenPosition), + // ), + // ), ], ), ), ); } else { - if ((i > 0 || i < tokenPositions.length - 1) && - tokenPositions[i + 1].hideContent && - tokenPositions[i - 1].hideContent) { - return WidgetSpan( - child: GestureDetector( - onTap: onClick != null - ? () => onClick?.call(tokenPosition) - : null, - child: HiddenText(text: substring, style: style(context)), - ), - ); - } + // if ((i > 0 || i < tokenPositions.length - 1) && + // tokenPositions[i + 1].hideContent && + // tokenPositions[i - 1].hideContent) { + // return WidgetSpan( + // child: GestureDetector( + // onTap: onClick != null + // ? () => onClick?.call(tokenPosition) + // : null, + // child: HiddenText( + // text: substring, + // style: style(context), + // ), + // ), + // ); + // } return LinkifySpan( text: substring, - style: style(context), + style: renderer.style(context), options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( decoration: TextDecoration.underline, @@ -423,3 +366,48 @@ class MessageTextWidget extends StatelessWidget { ); } } + +// class HiddenText extends StatelessWidget { +// final String text; +// final TextStyle style; + +// const HiddenText({ +// super.key, +// required this.text, +// required this.style, +// }); + +// @override +// Widget build(BuildContext context) { +// final TextPainter textPainter = TextPainter( +// text: TextSpan(text: text, style: style), +// textDirection: TextDirection.ltr, +// )..layout(); + +// final textWidth = textPainter.size.width; +// final textHeight = textPainter.size.height; + +// textPainter.dispose(); + +// return SizedBox( +// height: textHeight, +// child: Stack( +// children: [ +// Container( +// width: textWidth, +// height: textHeight, +// color: Colors.transparent, +// ), +// Positioned( +// bottom: 0, +// child: Container( +// width: textWidth, +// height: 1, +// color: style.color, +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/pangea/toolbar/widgets/overlay_center_content.dart b/lib/pangea/toolbar/widgets/overlay_center_content.dart index fcc79e2cc..d618848a8 100644 --- a/lib/pangea/toolbar/widgets/overlay_center_content.dart +++ b/lib/pangea/toolbar/widgets/overlay_center_content.dart @@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_reactions.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/measure_render_box.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart'; @@ -29,7 +30,7 @@ class OverlayCenterContent extends StatelessWidget { final bool hasReactions; final bool isTransitionAnimation; - final bool transitionAnimationFinished; + final ReadingAssistanceMode? readingAssistanceMode; const OverlayCenterContent({ required this.event, @@ -46,58 +47,64 @@ class OverlayCenterContent extends StatelessWidget { this.onChangeMessageSize, this.sizeAnimation, this.isTransitionAnimation = false, - this.transitionAnimationFinished = false, + this.readingAssistanceMode, super.key, }); @override Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints(maxWidth: maxWidth), - child: Material( - type: MaterialType.transparency, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: event.senderId == event.room.client.userID - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - MeasureRenderBox( - onChange: onChangeMessageSize, - child: OverlayMessage( - event, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: chatController.choreographer.immersionMode, - controller: chatController, - overlayController: overlayController, - nextEvent: nextEvent, - prevEvent: prevEvent, - timeline: chatController.timeline!, - sizeAnimation: sizeAnimation, - // there's a split seconds between when the transition animation starts and - // when the sizeAnimation is set when the original dimensions need to be enforced - messageWidth: (sizeAnimation == null && isTransitionAnimation) - ? messageWidth - : null, - messageHeight: (sizeAnimation == null && isTransitionAnimation) - ? messageHeight - : null, - maxHeight: maxHeight, - isTransitionAnimation: isTransitionAnimation, - ), - ), - if (hasReactions) - Padding( - padding: const EdgeInsets.all(4), - child: SizedBox( - height: 20, - child: MessageReactions( - event, - chatController.timeline!, - ), + return IgnorePointer( + ignoring: !isTransitionAnimation && + readingAssistanceMode != ReadingAssistanceMode.messageMode, + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Material( + type: MaterialType.transparency, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: event.senderId == event.room.client.userID + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + MeasureRenderBox( + onChange: onChangeMessageSize, + child: OverlayMessage( + event, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: chatController.choreographer.immersionMode, + controller: chatController, + overlayController: overlayController, + nextEvent: nextEvent, + prevEvent: prevEvent, + timeline: chatController.timeline!, + sizeAnimation: sizeAnimation, + // there's a split seconds between when the transition animation starts and + // when the sizeAnimation is set when the original dimensions need to be enforced + messageWidth: (sizeAnimation == null && isTransitionAnimation) + ? messageWidth + : null, + messageHeight: + (sizeAnimation == null && isTransitionAnimation) + ? messageHeight + : null, + maxHeight: maxHeight, + isTransitionAnimation: isTransitionAnimation, + readingAssistanceMode: readingAssistanceMode, ), ), - ], + if (hasReactions) + Padding( + padding: const EdgeInsets.all(4), + child: SizedBox( + height: 20, + child: MessageReactions( + event, + chatController.timeline!, + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/pangea/toolbar/widgets/overlay_message.dart b/lib/pangea/toolbar/widgets/overlay_message.dart index 0ac8de2e0..9544fc954 100644 --- a/lib/pangea/toolbar/widgets/overlay_message.dart +++ b/lib/pangea/toolbar/widgets/overlay_message.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat/events/message_content.dart'; import 'package:fluffychat/pages/chat/events/reply_content.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -30,6 +31,7 @@ class OverlayMessage extends StatelessWidget { final double maxHeight; final bool isTransitionAnimation; + final ReadingAssistanceMode? readingAssistanceMode; const OverlayMessage( this.event, { @@ -45,6 +47,7 @@ class OverlayMessage extends StatelessWidget { this.prevEvent, this.sizeAnimation, this.isTransitionAnimation = false, + this.readingAssistanceMode, super.key, }); @@ -204,6 +207,7 @@ class OverlayMessage extends StatelessWidget { ? theme.colorScheme.onPrimary : theme.colorScheme.onSurface, isTransitionAnimation: isTransitionAnimation, + readingAssistanceMode: readingAssistanceMode, ), if (event.hasAggregatedEvents( timeline,