feat: add clicking for tokens in HTML messages (#1510)
This commit is contained in:
parent
383fe50c7f
commit
f59b31ce9c
2 changed files with 178 additions and 23 deletions
|
|
@ -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<PangeaToken>? 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<PangeaToken> 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('</token>');
|
||||
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<token offset=\"${token.text.offset}\" length=\"${token.text.length}\">$tokenText</token>$after";
|
||||
}
|
||||
|
||||
final newElement = dom.Element.html('<span>$tokenizedText</span>');
|
||||
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<String> 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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue