diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 69f37136c..f0fa968f2 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1250,7 +1250,7 @@ class ChatController extends State void pickEmojiReactionAction(Iterable allReactionEvents) async { // #Pangea - MatrixState.pAnyState.closeAllOverlays(); + closeSelectionOverlay(); // Pangea# _allReactionEvents = allReactionEvents; emojiPickerType = EmojiPickerType.reaction; @@ -1271,9 +1271,19 @@ class ChatController extends State // Pangea# } + // #Pangea + /// Close the combined selection view overlay and clear the message + /// text and selection stored for the text in that overlay + void closeSelectionOverlay() { + MatrixState.pAnyState.closeAllOverlays(); + textSelection.clearMessageText(); + textSelection.onSelection(null); + } + // Pangea# + void clearSelectedEvents() => setState(() { // #Pangea - MatrixState.pAnyState.closeAllOverlays(); + closeSelectionOverlay(); // Pangea# selectedEvents.clear(); showEmojiPicker = false; diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 61f9e1065..803869a75 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,8 +1,11 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart'; import 'package:flutter_html/flutter_html.dart'; @@ -18,12 +21,22 @@ class HtmlMessage extends StatelessWidget { final String html; final Room room; final Color textColor; + // #Pangea + final bool isOverlay; + final PangeaMessageEvent? pangeaMessageEvent; + final ChatController controller; + // Pangea# const HtmlMessage({ super.key, required this.html, required this.room, this.textColor = Colors.black, + // #Pangea + required this.isOverlay, + this.pangeaMessageEvent, + required this.controller, + // Pangea# }); dom.Node _linkifyHtml(dom.Node element) { @@ -58,6 +71,9 @@ class HtmlMessage extends StatelessWidget { @override Widget build(BuildContext context) { + // #Pangea + controller.textSelection.setMessageText(html); + // Pangea# final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final linkColor = textColor.withAlpha(150); @@ -76,21 +92,16 @@ class HtmlMessage extends StatelessWidget { // there is no need to pre-validate the html, as we validate it while rendering // #Pangea - return MouseRegion( - // onHover: messageToolbar?.onMouseRegionUpdate, - child: SelectionArea( - // onSelectionChanged: (SelectedContent? selection) => - // messageToolbar?.onTextSelection( - // selectedContent: selection, - // context: context, - // ), - // focusNode: messageToolbar?.focusNode, - // contextMenuBuilder: (context, state) => - // messageToolbar?.contextMenuOverride( - // context: context, - // contentSelection: state, - // ) ?? - // const SizedBox(), + return SelectionArea( + onSelectionChanged: (SelectedContent? selection) { + controller.textSelection.onSelection(selection?.plainText); + }, + child: GestureDetector( + onTap: () { + if (pangeaMessageEvent != null && !isOverlay) { + controller.showToolbar(pangeaMessageEvent!); + } + }, // Pangea# child: Html.fromElement( documentElement: element as dom.Element, @@ -173,11 +184,6 @@ class HtmlMessage extends StatelessWidget { ), ), ); - // ), - // ], - // ), - // ), - // ); } static const Set fallbackTextTags = {'tg-forward'}; @@ -303,7 +309,6 @@ class ImageExtension extends HtmlExtension { uri: mxcUrl, width: width ?? height ?? defaultDimension, height: height ?? width ?? defaultDimension, - cacheKey: mxcUrl.toString(), ), ), ); diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index ae20d7a20..92bde721b 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -3,13 +3,13 @@ import 'dart:math'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; @@ -205,6 +205,11 @@ class MessageContent extends StatelessWidget { html: html, textColor: textColor, room: event.room, + // #Pangea + isOverlay: isOverlay, + controller: controller, + pangeaMessageEvent: pangeaMessageEvent, + // Pangea# ); } // else we fall through to the normal message rendering @@ -285,8 +290,8 @@ class MessageContent extends StatelessWidget { final bigEmotes = event.onlyEmotes && event.numberEmotes > 0 && event.numberEmotes <= 10; + // #Pangea - // return Linkify( final messageTextStyle = TextStyle( overflow: TextOverflow.ellipsis, color: textColor, @@ -314,38 +319,36 @@ class MessageContent extends StatelessWidget { ), ); } + // Pangea# - return SelectableLinkify( - onSelectionChanged: (selection, cause) { - if (isOverlay) { - controller.textSelection.onTextSelection(selection); - } - }, - onTap: () { - if (pangeaMessageEvent != null && !isOverlay) { - HapticFeedback.mediumImpact(); - controller.showToolbar(pangeaMessageEvent!); - } - }, - enableInteractiveSelection: isOverlay, - // Pangea# - text: event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - ), - style: TextStyle( - color: textColor, - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: event.redacted ? TextDecoration.lineThrough : null, - ), - options: const LinkifyOptions(humanize: false), - linkStyle: TextStyle( - color: textColor.withAlpha(150), - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: TextDecoration.underline, - decorationColor: textColor.withAlpha(150), + return + // #Pangea + ToolbarSelectionArea( + controller: controller, + pangeaMessageEvent: pangeaMessageEvent, + isOverlay: isOverlay, + child: + // Pangea# + Linkify( + text: event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + ), + style: TextStyle( + color: textColor, + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: textColor.withAlpha(150), + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: TextDecoration.underline, + decorationColor: textColor.withAlpha(150), + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ), - onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ); } case EventTypes.CallInvite: diff --git a/lib/pangea/widgets/chat/message_text_selection.dart b/lib/pangea/widgets/chat/message_text_selection.dart index 6263738f4..2396d08bf 100644 --- a/lib/pangea/widgets/chat/message_text_selection.dart +++ b/lib/pangea/widgets/chat/message_text_selection.dart @@ -1,37 +1,41 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - +/// Contains information about the text currently being shown in a +/// toolbar overlay message and any selection within that text. +/// The ChatController contains one instance of this class, and it's values +/// should be updated each time an overlay is openned or closed, or when +/// an overlay's text selection changes. class MessageTextSelection { + /// The currently selected text in the overlay message. String? selectedText; - String messageText = ""; + + /// The full text displayed in the overlay message. + String? messageText; + + /// A stream that emits the currently selected text whenever it changes. final StreamController selectionStream = StreamController.broadcast(); - void setMessageText(String text) { - messageText = text; - } + /// Sets messageText to match the text currently being displayed in the overlay. + /// Text in messages is displayed in a variety of ways, i.e., direct message content, + /// translation, HTML rendered message, etc. This method should be called wherever the + /// text displayed in the overlay is determined. + void setMessageText(String text) => messageText = text; - void onTextSelection(TextSelection selection) => selection.isCollapsed == true - ? clearTextSelection() - : setTextSelection(selection); + /// Clears the messageText value. Called when the message selection overlay is closed. + void clearMessageText() => messageText = null; - void setTextSelection(TextSelection selection) { - selectedText = selection.textInside(messageText); - if (BrowserContextMenu.enabled && kIsWeb) { - BrowserContextMenu.disableContextMenu(); - } + /// Updates the selectedText value and emits it to the selectionStream. + void onSelection(String? text) { + text == null || text.isEmpty ? selectedText = null : selectedText = text; selectionStream.add(selectedText); } - void clearTextSelection() { - selectedText = null; - if (kIsWeb && !BrowserContextMenu.enabled) { - BrowserContextMenu.enableContextMenu(); - } - selectionStream.add(selectedText); + /// Returns the index of the selected text within the message text. + /// If the selected text is not found, returns null. + int? get offset { + if (selectedText == null || messageText == null) return null; + final index = messageText!.indexOf(selectedText!); + return index > -1 ? index : null; } - - int get offset => messageText.indexOf(selectedText!); } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 943269bdb..4d41918a8 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; class MessageToolbar extends StatefulWidget { final MessageTextSelection textSelection; @@ -140,6 +141,7 @@ class MessageToolbarState extends State { void showDefinition() { debugPrint("show definition"); if (widget.textSelection.selectedText == null || + widget.textSelection.messageText == null || widget.textSelection.selectedText!.isEmpty) { toolbarContent = const SelectToDefine(); return; @@ -148,7 +150,7 @@ class MessageToolbarState extends State { toolbarContent = WordDataCard( word: widget.textSelection.selectedText!, wordLang: widget.pangeaMessageEvent.messageDisplayLangCode, - fullText: widget.textSelection.messageText, + fullText: widget.textSelection.messageText!, fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode, hasInfo: true, room: widget.controller.room, @@ -276,3 +278,35 @@ class MessageToolbarState extends State { ); } } + +class ToolbarSelectionArea extends StatelessWidget { + final ChatController controller; + final PangeaMessageEvent? pangeaMessageEvent; + final bool isOverlay; + final Widget child; + + const ToolbarSelectionArea({ + required this.controller, + this.pangeaMessageEvent, + this.isOverlay = false, + required this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SelectionArea( + onSelectionChanged: (SelectedContent? selection) { + controller.textSelection.onSelection(selection?.plainText); + }, + child: GestureDetector( + onTap: () { + if (pangeaMessageEvent != null && !isOverlay) { + controller.showToolbar(pangeaMessageEvent!); + } + }, + child: child, + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 10e75a47a..278c7d7ed 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -52,7 +52,8 @@ class MessageTranslationCardState extends State { Future translateSelection() async { if (widget.selection.selectedText == null || l1Code == null || - l2Code == null) { + l2Code == null || + widget.selection.messageText == null) { selectionTranslation = null; return; } @@ -64,7 +65,7 @@ class MessageTranslationCardState extends State { final resp = await FullTextTranslationRepo.translate( accessToken: accessToken, request: FullTextTranslationRequestModel( - text: widget.selection.messageText, + text: widget.selection.messageText!, tgtLang: l1Code!, userL1: l1Code!, userL2: l2Code!, diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 3ff10d4c7..0115f2f6c 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -134,37 +135,31 @@ class PangeaRichTextState extends State { } //TODO - take out of build function of every message - final Widget richText = SelectableText.rich( - onSelectionChanged: (selection, cause) { - if (widget.isOverlay) { - widget.controller.textSelection.onTextSelection(selection); - } - }, - onTap: () { - if (!widget.isOverlay) { - widget.controller.showToolbar(widget.pangeaMessageEvent); - } - }, - enableInteractiveSelection: widget.isOverlay, - TextSpan( - text: textSpan, - style: widget.style, - children: [ - if (_fetchingRepresentation) - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(left: 5.0), - child: SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: AppConfig.secondaryColor, + final Widget richText = ToolbarSelectionArea( + isOverlay: widget.isOverlay, + pangeaMessageEvent: widget.pangeaMessageEvent, + controller: widget.controller, + child: RichText( + text: TextSpan( + text: textSpan, + style: widget.style, + children: [ + if (_fetchingRepresentation) + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(left: 5.0), + child: SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppConfig.secondaryColor, + ), ), ), ), - ), - ], + ], + ), ), );