diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart
index 866b680c6..8e625f031 100644
--- a/lib/pages/chat/events/html_message.dart
+++ b/lib/pages/chat/events/html_message.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@@ -10,7 +11,9 @@ 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/error_handler.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/widgets/avatar.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import '../../../utils/url_launcher.dart';
@@ -26,6 +29,9 @@ class HtmlMessage extends StatelessWidget {
final Event event;
final Event? nextEvent;
final Event? prevEvent;
+
+ final bool Function(PangeaToken)? isSelected;
+ final void Function(PangeaToken)? onClick;
// Pangea#
const HtmlMessage({
@@ -40,6 +46,8 @@ class HtmlMessage extends StatelessWidget {
required this.controller,
this.nextEvent,
this.prevEvent,
+ this.isSelected,
+ this.onClick,
// Pangea#
});
@@ -73,6 +81,73 @@ class HtmlMessage extends StatelessWidget {
return element;
}
+ // #Pangea
+ List? get tokens =>
+ pangeaMessageEvent!.messageDisplayRepresentation?.tokens;
+
+ PangeaToken? getToken(
+ String text,
+ int offset,
+ int length,
+ ) =>
+ tokens?.firstWhereOrNull(
+ (token) => token.text.offset == offset && token.text.length == length,
+ );
+
+ /// Wrap token spans in token tags so styling / functions can be applied
+ dom.Node _tokenizeHtml(
+ dom.Node element,
+ String fullHtml,
+ List remainingTokens,
+ ) {
+ for (final node in element.nodes) {
+ node.replaceWith(_tokenizeHtml(node, fullHtml, remainingTokens));
+ }
+
+ if (element is dom.Text) {
+ // once a text element in reached in the HTML tree, find and
+ // wrap all the spans with matching tokens until all tokens
+ // have been wrapped or no more text elements remain
+ String tokenizedText = element.text;
+ while (remainingTokens.isNotEmpty) {
+ final tokenText = remainingTokens.first.text.content;
+
+ int startIndex = tokenizedText.lastIndexOf('');
+ startIndex = startIndex == -1 ? 0 : startIndex + 8;
+ final int tokenIndex = tokenizedText.indexOf(
+ tokenText,
+ startIndex,
+ );
+
+ // if the token is not found in the text, check if the token exist in the full HTML.
+ // If not, remove the token and continue. If so, break to move on to the next node in the HTML.
+ if (tokenIndex == -1) {
+ final fullHtmlIndex = fullHtml.indexOf(tokenText);
+ if (fullHtmlIndex == -1) {
+ remainingTokens.removeAt(0);
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ final token = remainingTokens.removeAt(0);
+ final tokenEnd = tokenIndex + tokenText.length;
+ final before = tokenizedText.substring(0, tokenIndex);
+ final after = tokenizedText.substring(tokenEnd);
+
+ tokenizedText =
+ "$before$tokenText$after";
+ }
+
+ final newElement = dom.Element.html('$tokenizedText');
+ return newElement;
+ }
+
+ return element;
+ }
+ // Pangea#
+
@override
Widget build(BuildContext context) {
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
@@ -89,7 +164,24 @@ class HtmlMessage extends StatelessWidget {
padding: HtmlPaddings.only(left: 6, bottom: 0),
);
- final element = _linkifyHtml(HtmlParser.parseHTML(html));
+ // #Pangea
+ // final element = _linkifyHtml(HtmlParser.parseHTML(html));
+ dom.Node element = _linkifyHtml(HtmlParser.parseHTML(html));
+ if (tokens != null && element is dom.Element) {
+ try {
+ element = _tokenizeHtml(element, element.innerHtml, List.from(tokens!));
+ } catch (e, s) {
+ ErrorHandler.logError(
+ e: e,
+ s: s,
+ data: {
+ 'html': html,
+ 'tokens': tokens,
+ },
+ );
+ }
+ }
+ // Pangea#
// there is no need to pre-validate the html, as we validate it while rendering
// #Pangea
@@ -170,6 +262,18 @@ class HtmlMessage extends StatelessWidget {
const ImageExtension(),
FontColorExtension(),
FallbackTextExtension(fontSize: fontSize),
+ // #Pangea
+ if (pangeaMessageEvent != null)
+ TokenExtension(
+ style: AppConfig.messageTextStyle(
+ pangeaMessageEvent!.event,
+ textColor,
+ ),
+ getToken: getToken,
+ isSelected: isSelected,
+ onClick: onClick,
+ ),
+ // Pangea#
],
onLinkTap: (url, _, element) => UrlLauncher(
context,
@@ -236,9 +340,55 @@ class HtmlMessage extends StatelessWidget {
'rt',
// Workaround for https://github.com/krille-chan/fluffychat/issues/507
...fallbackTextTags,
+ // #Pangea
+ 'token',
+ // Pangea#
};
}
+// #Pangea
+class TokenExtension extends HtmlExtension {
+ final TextStyle style;
+ final PangeaToken? Function(String, int, int) getToken;
+ final bool Function(PangeaToken)? isSelected;
+ final void Function(PangeaToken)? onClick;
+
+ const TokenExtension({
+ required this.style,
+ required this.getToken,
+ this.isSelected,
+ this.onClick,
+ });
+
+ @override
+ Set get supportedTags => {'token'};
+
+ @override
+ InlineSpan build(ExtensionContext context) {
+ final token = getToken(
+ context.attributes['offset'] ?? '',
+ int.tryParse(context.attributes['offset'] ?? '') ?? 0,
+ int.tryParse(context.attributes['length'] ?? '') ?? 0,
+ );
+
+ final selected =
+ token != null && isSelected != null ? isSelected!.call(token) : false;
+
+ final backgroundColor =
+ selected ? AppConfig.primaryColor.withAlpha(80) : Colors.transparent;
+
+ return TextSpan(
+ recognizer: TapGestureRecognizer()
+ ..onTap = onClick != null && token != null
+ ? () => onClick?.call(token)
+ : null,
+ text: context.innerHtml,
+ style: style.merge(TextStyle(backgroundColor: backgroundColor)),
+ );
+ }
+}
+// Pangea#
+
class FontColorExtension extends HtmlExtension {
static const String colorAttribute = 'color';
static const String mxColorAttribute = 'data-mx-color';
diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart
index 215d57f70..acf3a6341 100644
--- a/lib/pages/chat/events/message_content.dart
+++ b/lib/pages/chat/events/message_content.dart
@@ -10,6 +10,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_rich_text.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/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';
@@ -117,6 +118,28 @@ class MessageContent extends StatelessWidget {
// ),
// );
// }
+
+ void onClick(PangeaToken token) {
+ token = pangeaMessageEvent?.messageDisplayRepresentation
+ ?.getClosestNonPunctToken(token) ??
+ token;
+
+ if (overlayController != null) {
+ overlayController?.onClickOverlayMessageToken(token);
+ return;
+ }
+
+ controller.showToolbar(
+ pangeaMessageEvent!.event,
+ pangeaMessageEvent: pangeaMessageEvent,
+ selectedToken: token,
+ );
+ }
+
+ bool isSelected(PangeaToken token) {
+ return overlayController!.isTokenSelected(token) ||
+ overlayController!.isTokenHighlighted(token);
+ }
// Pangea#
@override
@@ -215,6 +238,8 @@ class MessageContent extends StatelessWidget {
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
+ isSelected: overlayController != null ? isSelected : null,
+ onClick: onClick,
// Pangea#
);
}
@@ -321,28 +346,8 @@ class MessageContent extends StatelessWidget {
tokens:
pangeaMessageEvent!.messageDisplayRepresentation?.tokens,
style: messageTextStyle,
- onClick: (token) {
- token = pangeaMessageEvent?.messageDisplayRepresentation
- ?.getClosestNonPunctToken(token) ??
- token;
-
- if (overlayController != null) {
- overlayController?.onClickOverlayMessageToken(token);
- return;
- }
-
- controller.showToolbar(
- pangeaMessageEvent!.event,
- pangeaMessageEvent: pangeaMessageEvent,
- selectedToken: token,
- );
- },
- isSelected: overlayController != null
- ? (token) {
- return overlayController!.isTokenSelected(token) ||
- overlayController!.isTokenHighlighted(token);
- }
- : null,
+ onClick: onClick,
+ isSelected: overlayController != null ? isSelected : null,
);
}