diff --git a/lib/pangea/events/utils/message_text_util.dart b/lib/pangea/events/utils/message_text_util.dart index 825c19fc3..f23fc30f2 100644 --- a/lib/pangea/events/utils/message_text_util.dart +++ b/lib/pangea/events/utils/message_text_util.dart @@ -25,14 +25,16 @@ class MessageTextUtil { final List tokenPositions = []; int globalIndex = 0; - for (final token - in pangeaMessageEvent.messageDisplayRepresentation!.tokens!) { + final tokens = pangeaMessageEvent.messageDisplayRepresentation!.tokens!; + int pointer = 0; + while (pointer < tokens.length) { + final token = tokens[pointer]; final start = token.start; final end = token.end; // Calculate the number of grapheme clusters up to the start and end positions final int startIndex = messageCharacters.take(start).length; - final int endIndex = messageCharacters.take(end).length; + int endIndex = messageCharacters.take(end).length; final hideContent = messageAnalyticsEntry?.isTokenInHiddenWordActivity(token) ?? false; @@ -40,21 +42,40 @@ class MessageTextUtil { final hasHiddenContent = messageAnalyticsEntry?.hasHiddenWordActivity ?? false; + // if this is white space, add position without token if (globalIndex < startIndex) { tokenPositions.add( TokenPosition( start: globalIndex, end: startIndex, + tokenStart: globalIndex, + tokenEnd: startIndex, hideContent: false, selected: (isSelected?.call(token) ?? false) && !hasHiddenContent, ), ); } + // group tokens with punctuation next to it so punctuation doesn't cause newline + final List followingPunctTokens = []; + int nextTokenPointer = pointer + 1; + while (nextTokenPointer < tokens.length) { + final nextToken = tokens[nextTokenPointer]; + if (nextToken.pos == 'PUNCT') { + followingPunctTokens.add(nextToken); + nextTokenPointer++; + endIndex = messageCharacters.take(nextToken.end).length; + continue; + } + break; + } + tokenPositions.add( TokenPosition( start: startIndex, end: endIndex, + tokenStart: startIndex, + tokenEnd: messageCharacters.take(end).length, token: token, hideContent: hideContent, selected: (isSelected?.call(token) ?? false) && @@ -62,7 +83,10 @@ class MessageTextUtil { !hasHiddenContent, ), ); + globalIndex = endIndex; + pointer = nextTokenPointer; + continue; } return tokenPositions; diff --git a/lib/pangea/toolbar/widgets/message_token_text.dart b/lib/pangea/toolbar/widgets/message_token_text.dart index 233f87b33..61d547dc8 100644 --- a/lib/pangea/toolbar/widgets/message_token_text.dart +++ b/lib/pangea/toolbar/widgets/message_token_text.dart @@ -70,8 +70,18 @@ class MessageTokenText extends StatelessWidget { } class TokenPosition { + /// Start index of the full substring in the message final int start; + + /// End index of the full substring in the message final int end; + + /// Start index of the token in the message + final int tokenStart; + + /// End index of the token in the message + final int tokenEnd; + final bool selected; final bool hideContent; final PangeaToken? token; @@ -79,6 +89,8 @@ class TokenPosition { const TokenPosition({ required this.start, required this.end, + required this.tokenStart, + required this.tokenEnd, required this.hideContent, required this.selected, this.token, @@ -225,6 +237,19 @@ class MessageTextWidget extends StatelessWidget { ), ); } + + // if the tokenPosition is a combination of the token and following punctuation + // split them so that only the token itself is highlighted when clicked + String firstSubstring = substring; + String secondSubstring = ''; + + if (tokenPosition.end != tokenPosition.tokenEnd) { + final splitIndex = (tokenPosition.end - tokenPosition.start) - + (tokenPosition.end - tokenPosition.tokenEnd); + firstSubstring = substring.substring(0, splitIndex); + secondSubstring = substring.substring(splitIndex); + } + return WidgetSpan( child: MouseRegion( cursor: SystemMouseCursors.click, @@ -233,21 +258,40 @@ class MessageTextWidget extends StatelessWidget { ? () => onClick?.call(tokenPosition) : null, child: RichText( - text: LinkifySpan( - text: substring, - style: style.merge( - TextStyle( - backgroundColor: backgroundColor, + text: TextSpan( + children: [ + LinkifySpan( + text: firstSubstring, + style: style.merge( + TextStyle( + backgroundColor: backgroundColor, + ), + ), + linkStyle: TextStyle( + decoration: TextDecoration.underline, + color: + Theme.of(context).brightness == Brightness.light + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onPrimary, + ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), ), - ), - linkStyle: TextStyle( - decoration: TextDecoration.underline, - color: Theme.of(context).brightness == Brightness.light - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onPrimary, - ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), + if (secondSubstring.isNotEmpty) + LinkifySpan( + text: secondSubstring, + style: style, + linkStyle: TextStyle( + decoration: TextDecoration.underline, + color: Theme.of(context).brightness == + Brightness.light + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onPrimary, + ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), + ), + ], ), ), ),