Merge pull request #654 from pangeachat/html-message-toolbar

enable toolbar on click for html messages
pull/1384/head
ggurdin 1 year ago committed by GitHub
commit 2a83d3099f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1250,7 +1250,7 @@ class ChatController extends State<ChatPageWithRoom>
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async { void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
// #Pangea // #Pangea
MatrixState.pAnyState.closeAllOverlays(); closeSelectionOverlay();
// Pangea# // Pangea#
_allReactionEvents = allReactionEvents; _allReactionEvents = allReactionEvents;
emojiPickerType = EmojiPickerType.reaction; emojiPickerType = EmojiPickerType.reaction;
@ -1271,9 +1271,19 @@ class ChatController extends State<ChatPageWithRoom>
// Pangea# // Pangea#
} }
void clearSelectedEvents() => setState(() {
// #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(); MatrixState.pAnyState.closeAllOverlays();
textSelection.clearMessageText();
textSelection.onSelection(null);
}
// Pangea#
void clearSelectedEvents() => setState(() {
// #Pangea
closeSelectionOverlay();
// Pangea# // Pangea#
selectedEvents.clear(); selectedEvents.clear();
showEmojiPicker = false; showEmojiPicker = false;

@ -1,8 +1,11 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.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/avatar.dart';
import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart';
import 'package:flutter_highlighter/themes/shades-of-purple.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
@ -18,12 +21,22 @@ class HtmlMessage extends StatelessWidget {
final String html; final String html;
final Room room; final Room room;
final Color textColor; final Color textColor;
// #Pangea
final bool isOverlay;
final PangeaMessageEvent? pangeaMessageEvent;
final ChatController controller;
// Pangea#
const HtmlMessage({ const HtmlMessage({
super.key, super.key,
required this.html, required this.html,
required this.room, required this.room,
this.textColor = Colors.black, this.textColor = Colors.black,
// #Pangea
required this.isOverlay,
this.pangeaMessageEvent,
required this.controller,
// Pangea#
}); });
dom.Node _linkifyHtml(dom.Node element) { dom.Node _linkifyHtml(dom.Node element) {
@ -58,6 +71,9 @@ class HtmlMessage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// #Pangea
controller.textSelection.setMessageText(html);
// Pangea#
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
final linkColor = textColor.withAlpha(150); 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 // there is no need to pre-validate the html, as we validate it while rendering
// #Pangea // #Pangea
return MouseRegion( return SelectionArea(
// onHover: messageToolbar?.onMouseRegionUpdate, onSelectionChanged: (SelectedContent? selection) {
child: SelectionArea( controller.textSelection.onSelection(selection?.plainText);
// onSelectionChanged: (SelectedContent? selection) => },
// messageToolbar?.onTextSelection( child: GestureDetector(
// selectedContent: selection, onTap: () {
// context: context, if (pangeaMessageEvent != null && !isOverlay) {
// ), controller.showToolbar(pangeaMessageEvent!);
// focusNode: messageToolbar?.focusNode, }
// contextMenuBuilder: (context, state) => },
// messageToolbar?.contextMenuOverride(
// context: context,
// contentSelection: state,
// ) ??
// const SizedBox(),
// Pangea# // Pangea#
child: Html.fromElement( child: Html.fromElement(
documentElement: element as dom.Element, documentElement: element as dom.Element,
@ -173,11 +184,6 @@ class HtmlMessage extends StatelessWidget {
), ),
), ),
); );
// ),
// ],
// ),
// ),
// );
} }
static const Set<String> fallbackTextTags = {'tg-forward'}; static const Set<String> fallbackTextTags = {'tg-forward'};
@ -303,7 +309,6 @@ class ImageExtension extends HtmlExtension {
uri: mxcUrl, uri: mxcUrl,
width: width ?? height ?? defaultDimension, width: width ?? height ?? defaultDimension,
height: height ?? width ?? defaultDimension, height: height ?? width ?? defaultDimension,
cacheKey: mxcUrl.toString(),
), ),
), ),
); );

@ -3,13 +3,13 @@ import 'dart:math';
import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/video_player.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/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/pangea/widgets/igc/pangea_rich_text.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -205,6 +205,11 @@ class MessageContent extends StatelessWidget {
html: html, html: html,
textColor: textColor, textColor: textColor,
room: event.room, room: event.room,
// #Pangea
isOverlay: isOverlay,
controller: controller,
pangeaMessageEvent: pangeaMessageEvent,
// Pangea#
); );
} }
// else we fall through to the normal message rendering // else we fall through to the normal message rendering
@ -285,8 +290,8 @@ class MessageContent extends StatelessWidget {
final bigEmotes = event.onlyEmotes && final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 && event.numberEmotes > 0 &&
event.numberEmotes <= 10; event.numberEmotes <= 10;
// #Pangea // #Pangea
// return Linkify(
final messageTextStyle = TextStyle( final messageTextStyle = TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: textColor, color: textColor,
@ -314,21 +319,17 @@ class MessageContent extends StatelessWidget {
), ),
); );
} }
// Pangea#
return SelectableLinkify( return
onSelectionChanged: (selection, cause) { // #Pangea
if (isOverlay) { ToolbarSelectionArea(
controller.textSelection.onTextSelection(selection); controller: controller,
} pangeaMessageEvent: pangeaMessageEvent,
}, isOverlay: isOverlay,
onTap: () { child:
if (pangeaMessageEvent != null && !isOverlay) {
HapticFeedback.mediumImpact();
controller.showToolbar(pangeaMessageEvent!);
}
},
enableInteractiveSelection: isOverlay,
// Pangea# // Pangea#
Linkify(
text: event.calcLocalizedBodyFallback( text: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
hideReply: true, hideReply: true,
@ -336,7 +337,8 @@ class MessageContent extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: textColor, color: textColor,
fontSize: bigEmotes ? fontSize * 3 : fontSize, fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: event.redacted ? TextDecoration.lineThrough : null, decoration:
event.redacted ? TextDecoration.lineThrough : null,
), ),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
@ -346,6 +348,7 @@ class MessageContent extends StatelessWidget {
decorationColor: textColor.withAlpha(150), decorationColor: textColor.withAlpha(150),
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
); );
} }
case EventTypes.CallInvite: case EventTypes.CallInvite:

@ -1,37 +1,41 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; /// Contains information about the text currently being shown in a
import 'package:flutter/services.dart'; /// 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 { class MessageTextSelection {
/// The currently selected text in the overlay message.
String? selectedText; 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<String?> selectionStream = final StreamController<String?> selectionStream =
StreamController<String?>.broadcast(); StreamController<String?>.broadcast();
void setMessageText(String text) { /// Sets messageText to match the text currently being displayed in the overlay.
messageText = text; /// 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 /// Clears the messageText value. Called when the message selection overlay is closed.
? clearTextSelection() void clearMessageText() => messageText = null;
: setTextSelection(selection);
void setTextSelection(TextSelection selection) { /// Updates the selectedText value and emits it to the selectionStream.
selectedText = selection.textInside(messageText); void onSelection(String? text) {
if (BrowserContextMenu.enabled && kIsWeb) { text == null || text.isEmpty ? selectedText = null : selectedText = text;
BrowserContextMenu.disableContextMenu();
}
selectionStream.add(selectedText); selectionStream.add(selectedText);
} }
void clearTextSelection() { /// Returns the index of the selected text within the message text.
selectedText = null; /// If the selected text is not found, returns null.
if (kIsWeb && !BrowserContextMenu.enabled) { int? get offset {
BrowserContextMenu.enableContextMenu(); if (selectedText == null || messageText == null) return null;
} final index = messageText!.indexOf(selectedText!);
selectionStream.add(selectedText); return index > -1 ? index : null;
} }
int get offset => messageText.indexOf(selectedText!);
} }

@ -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/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class MessageToolbar extends StatefulWidget { class MessageToolbar extends StatefulWidget {
final MessageTextSelection textSelection; final MessageTextSelection textSelection;
@ -140,6 +141,7 @@ class MessageToolbarState extends State<MessageToolbar> {
void showDefinition() { void showDefinition() {
debugPrint("show definition"); debugPrint("show definition");
if (widget.textSelection.selectedText == null || if (widget.textSelection.selectedText == null ||
widget.textSelection.messageText == null ||
widget.textSelection.selectedText!.isEmpty) { widget.textSelection.selectedText!.isEmpty) {
toolbarContent = const SelectToDefine(); toolbarContent = const SelectToDefine();
return; return;
@ -148,7 +150,7 @@ class MessageToolbarState extends State<MessageToolbar> {
toolbarContent = WordDataCard( toolbarContent = WordDataCard(
word: widget.textSelection.selectedText!, word: widget.textSelection.selectedText!,
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode, wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
fullText: widget.textSelection.messageText, fullText: widget.textSelection.messageText!,
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode, fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
hasInfo: true, hasInfo: true,
room: widget.controller.room, room: widget.controller.room,
@ -276,3 +278,35 @@ class MessageToolbarState extends State<MessageToolbar> {
); );
} }
} }
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,
),
);
}
}

@ -52,7 +52,8 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
Future<void> translateSelection() async { Future<void> translateSelection() async {
if (widget.selection.selectedText == null || if (widget.selection.selectedText == null ||
l1Code == null || l1Code == null ||
l2Code == null) { l2Code == null ||
widget.selection.messageText == null) {
selectionTranslation = null; selectionTranslation = null;
return; return;
} }
@ -64,7 +65,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
final resp = await FullTextTranslationRepo.translate( final resp = await FullTextTranslationRepo.translate(
accessToken: accessToken, accessToken: accessToken,
request: FullTextTranslationRequestModel( request: FullTextTranslationRequestModel(
text: widget.selection.messageText, text: widget.selection.messageText!,
tgtLang: l1Code!, tgtLang: l1Code!,
userL1: l1Code!, userL1: l1Code!,
userL2: l2Code!, userL2: l2Code!,

@ -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/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.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:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -134,19 +135,12 @@ class PangeaRichTextState extends State<PangeaRichText> {
} }
//TODO - take out of build function of every message //TODO - take out of build function of every message
final Widget richText = SelectableText.rich( final Widget richText = ToolbarSelectionArea(
onSelectionChanged: (selection, cause) { isOverlay: widget.isOverlay,
if (widget.isOverlay) { pangeaMessageEvent: widget.pangeaMessageEvent,
widget.controller.textSelection.onTextSelection(selection); controller: widget.controller,
} child: RichText(
}, text: TextSpan(
onTap: () {
if (!widget.isOverlay) {
widget.controller.showToolbar(widget.pangeaMessageEvent);
}
},
enableInteractiveSelection: widget.isOverlay,
TextSpan(
text: textSpan, text: textSpan,
style: widget.style, style: widget.style,
children: [ children: [
@ -166,6 +160,7 @@ class PangeaRichTextState extends State<PangeaRichText> {
), ),
], ],
), ),
),
); );
return blur > 0 return blur > 0

Loading…
Cancel
Save