From c7722d64dd219dc84427ce0e9e4f30d3bf01eacb Mon Sep 17 00:00:00 2001 From: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Date: Sat, 11 Jul 2026 20:07:21 -0700 Subject: [PATCH 1/3] fix(mobile): highlight full multi-word mentions Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> --- .../lib/features/channels/message_content.dart | 2 +- .../features/channels/message_content_test.dart | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/mobile/lib/features/channels/message_content.dart b/mobile/lib/features/channels/message_content.dart index 143e6b9f36..73f92aa6e1 100644 --- a/mobile/lib/features/channels/message_content.dart +++ b/mobile/lib/features/channels/message_content.dart @@ -720,7 +720,7 @@ Iterable _mentionAliases(Iterable mentionNames) sync* { for (final name in mentionNames) { final trimmed = name.trim(); if (trimmed.isEmpty) continue; - yield trimmed; + yield trimmed.replaceAll(' ', '\u00A0'); final firstName = trimmed.split(RegExp(r'\s+')).first; if (firstName.isNotEmpty) { yield firstName; diff --git a/mobile/test/features/channels/message_content_test.dart b/mobile/test/features/channels/message_content_test.dart index 947c57e1ed..e22225476b 100644 --- a/mobile/test/features/channels/message_content_test.dart +++ b/mobile/test/features/channels/message_content_test.dart @@ -732,6 +732,23 @@ void main() { expect(find.text('@Alice'), findsOneWidget); }); + testWidgets('highlights an entire multi-word display name', ( + tester, + ) async { + await tester.pumpWidget( + _testable( + const MessageContent( + content: 'Hey @Kenny Lopez can you review this?', + mentionNames: {'pk1': 'Kenny Lopez'}, + ), + ), + ); + + expect(find.text('@Kenny Lopez'), findsOneWidget); + expect(find.text('@Kenny'), findsNothing); + expect(_allRichText(tester), isNot(contains('Lopez Lopez'))); + }); + testWidgets('renders unknown @mention as-is', (tester) async { await tester.pumpWidget( _testable( From 50af01a9085f427be1841eb6cf997cbeede49a15 Mon Sep 17 00:00:00 2001 From: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Date: Sat, 11 Jul 2026 20:33:05 -0700 Subject: [PATCH 2/3] docs(mobile): explain mention alias normalization Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> --- mobile/lib/features/channels/message_content.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/lib/features/channels/message_content.dart b/mobile/lib/features/channels/message_content.dart index 73f92aa6e1..1806bdc7c0 100644 --- a/mobile/lib/features/channels/message_content.dart +++ b/mobile/lib/features/channels/message_content.dart @@ -720,6 +720,8 @@ Iterable _mentionAliases(Iterable mentionNames) sync* { for (final name in mentionNames) { final trimmed = name.trim(); if (trimmed.isEmpty) continue; + // Message preprocessing replaces spaces in full-name mentions with + // non-breaking spaces, so the matching alias must use the same form. yield trimmed.replaceAll(' ', '\u00A0'); final firstName = trimmed.split(RegExp(r'\s+')).first; if (firstName.isNotEmpty) { From b0faeadf32b9ae84f5e65369e22524943a8f1386 Mon Sep 17 00:00:00 2001 From: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Date: Sat, 11 Jul 2026 20:37:19 -0700 Subject: [PATCH 3/3] refactor(mobile): share mention name normalization Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> --- mobile/lib/features/channels/message_content.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/lib/features/channels/message_content.dart b/mobile/lib/features/channels/message_content.dart index 1806bdc7c0..4ac6b4339d 100644 --- a/mobile/lib/features/channels/message_content.dart +++ b/mobile/lib/features/channels/message_content.dart @@ -112,10 +112,10 @@ class MessageContent extends HookConsumerWidget { var segment = mentionParts[i]; for (final name in mentionNames.values) { if (name.contains(' ')) { - final nbspName = name.replaceAll(' ', '\u00A0'); + final normalizedName = _markdownMentionName(name); segment = segment.replaceAllMapped( RegExp('@${RegExp.escape(name)}', caseSensitive: false), - (m) => '@$nbspName', + (m) => '@$normalizedName', ); } } @@ -716,13 +716,13 @@ RegExp _buildPrefixPattern({ ); } +String _markdownMentionName(String name) => name.replaceAll(' ', '\u00A0'); + Iterable _mentionAliases(Iterable mentionNames) sync* { for (final name in mentionNames) { final trimmed = name.trim(); if (trimmed.isEmpty) continue; - // Message preprocessing replaces spaces in full-name mentions with - // non-breaking spaces, so the matching alias must use the same form. - yield trimmed.replaceAll(' ', '\u00A0'); + yield _markdownMentionName(trimmed); final firstName = trimmed.split(RegExp(r'\s+')).first; if (firstName.isNotEmpty) { yield firstName;