diff --git a/lib/src/room.dart b/lib/src/room.dart index 1b940d35..59fe39bc 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -566,6 +566,12 @@ class Room { Map> get emotePacks => getImagePacksFlat(ImagePackUsage.emoticon); + /// returns the resolved mxid for a mention string, or null if none found + String getMention(String mention) => getParticipants() + .firstWhere((u) => u.mentionFragments.contains(mention), + orElse: () => null) + ?.id; + /// Sends a normal text message to this room. Returns the event ID generated /// by the server for this message. Future sendTextEvent(String message, @@ -585,12 +591,9 @@ class Room { 'body': message, }; if (parseMarkdown) { - final mentionMap = { - for (final user in getParticipants()) user.mention: user.id - }; final html = markdown(event['body'], - emotePacks: getImagePacksFlat(ImagePackUsage.emoticon), - mentionMap: mentionMap); + getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon), + getMention: getMention); // if the decoded html is the same as the body, there is no need in sending a formatted message if (HtmlUnescape().convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != event['body']) { diff --git a/lib/src/user.dart b/lib/src/user.dart index cc639684..9bf2d4b7 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -207,6 +207,23 @@ class User extends Event { } return '$identifier#$ourHash'; } + + /// Get the mention fragments for this user. + Set get mentionFragments { + if ((displayName?.isEmpty ?? true) || + {'[', ']', ':'}.any((c) => displayName.contains(c))) { + return {}; + } + var identifier = '@'; + // if we have non-word characters we need to surround with [] + if (!RegExp(r'^\w+$').hasMatch(displayName)) { + identifier += '[$displayName]'; + } else { + identifier += displayName; + } + final hash = _hash(id); + return {identifier, '$identifier#$hash'}; + } } String _hash(String s) { diff --git a/lib/src/utils/markdown.dart b/lib/src/utils/markdown.dart index 9d4c1441..10389b70 100644 --- a/lib/src/utils/markdown.dart +++ b/lib/src/utils/markdown.dart @@ -72,11 +72,13 @@ class SpoilerSyntax extends TagSyntax { } class EmoteSyntax extends InlineSyntax { - final Map> emotePacks; - EmoteSyntax(this.emotePacks) : super(r':(?:([-\w]+)~)?([-\w]+):'); + final Map> Function() getEmotePacks; + Map> emotePacks; + EmoteSyntax(this.getEmotePacks) : super(r':(?:([-\w]+)~)?([-\w]+):'); @override bool onMatch(InlineParser parser, Match match) { + emotePacks ??= getEmotePacks?.call() ?? >{}; final pack = match[1] ?? ''; final emote = match[2]; String mxc; @@ -182,29 +184,31 @@ class PillSyntax extends InlineSyntax { } class MentionSyntax extends InlineSyntax { - final Map mentionMap; - MentionSyntax(this.mentionMap) : super(r'(@(?:\[[^\]:]+\]|\w+)(?:#\w+)?)'); + final String Function(String) getMention; + MentionSyntax(this.getMention) : super(r'(@(?:\[[^\]:]+\]|\w+)(?:#\w+)?)'); @override bool onMatch(InlineParser parser, Match match) { + final mention = getMention?.call(match[1]); if ((match.start > 0 && !RegExp(r'[\s.!?:;\(]').hasMatch(match.input[match.start - 1])) || - !mentionMap.containsKey(match[1])) { + mention == null) { parser.addNode(Text(match[0])); return true; } - final identifier = mentionMap[match[1]]; final element = Element.text('a', htmlEscape.convert(match[1])); element.attributes['href'] = - htmlAttrEscape.convert('https://matrix.to/#/$identifier'); + htmlAttrEscape.convert('https://matrix.to/#/$mention'); parser.addNode(element); return true; } } -String markdown(String text, - {Map> emotePacks, - Map mentionMap}) { +String markdown( + String text, { + Map> Function() getEmotePacks, + String Function(String) getMention, +}) { var ret = markdownToHtml( text, extensionSet: ExtensionSet.commonMark, @@ -215,9 +219,9 @@ String markdown(String text, StrikethroughSyntax(), LinebreakSyntax(), SpoilerSyntax(), - EmoteSyntax(emotePacks ?? >{}), + EmoteSyntax(getEmotePacks), PillSyntax(), - MentionSyntax(mentionMap ?? {}), + MentionSyntax(getMention), InlineLatexSyntax(), ], ); diff --git a/test/markdown_test.dart b/test/markdown_test.dart index 0770d6ee..07477870 100644 --- a/test/markdown_test.dart +++ b/test/markdown_test.dart @@ -40,6 +40,7 @@ void main() { '@[Fast Fox]#123': '@fastfox:example.org', '@[">]': '@blah:example.org', }; + final getMention = (mention) => mentionMap[mention]; test('simple markdown', () { expect(markdown('hey *there* how are **you** doing?'), 'hey there how are you doing?'); @@ -67,16 +68,18 @@ void main() { expect(markdown('foxies\ncute'), 'foxies
\ncute'); }); test('emotes', () { - expect(markdown(':fox:', emotePacks: emotePacks), + expect(markdown(':fox:', getEmotePacks: () => emotePacks), ':fox:'); - expect(markdown(':user~fox:', emotePacks: emotePacks), + expect(markdown(':user~fox:', getEmotePacks: () => emotePacks), ':fox:'); - expect(markdown(':raccoon:', emotePacks: emotePacks), + expect(markdown(':raccoon:', getEmotePacks: () => emotePacks), ':raccoon:'); - expect(markdown(':invalid:', emotePacks: emotePacks), ':invalid:'); - expect(markdown(':invalid:?!', emotePacks: emotePacks), ':invalid:?!'); expect( - markdown(':room~invalid:', emotePacks: emotePacks), ':room~invalid:'); + markdown(':invalid:', getEmotePacks: () => emotePacks), ':invalid:'); + expect(markdown(':invalid:?!', getEmotePacks: () => emotePacks), + ':invalid:?!'); + expect(markdown(':room~invalid:', getEmotePacks: () => emotePacks), + ':room~invalid:'); }); test('pills', () { expect(markdown('Hey @sorunome:sorunome.de!'), @@ -95,17 +98,17 @@ void main() { 'Hey @sorunome:[::1]!'); }); test('mentions', () { - expect(markdown('Hey @Bob!', mentionMap: mentionMap), + expect(markdown('Hey @Bob!', getMention: getMention), 'Hey @Bob!'); - expect(markdown('How is @[Bob Ross] doing?', mentionMap: mentionMap), + expect(markdown('How is @[Bob Ross] doing?', getMention: getMention), 'How is @[Bob Ross] doing?'); expect( - markdown('Hey @invalid!', mentionMap: mentionMap), 'Hey @invalid!'); - expect(markdown('Hey @Fox#123!', mentionMap: mentionMap), + markdown('Hey @invalid!', getMention: getMention), 'Hey @invalid!'); + expect(markdown('Hey @Fox#123!', getMention: getMention), 'Hey @Fox#123!'); - expect(markdown('Hey @[Fast Fox]#123!', mentionMap: mentionMap), + expect(markdown('Hey @[Fast Fox]#123!', getMention: getMention), 'Hey @[Fast Fox]#123!'); - expect(markdown('Hey @[">]!', mentionMap: mentionMap), + expect(markdown('Hey @[">]!', getMention: getMention), 'Hey @[">]!'); }); test('latex', () { diff --git a/test/room_test.dart b/test/room_test.dart index f3835ea6..e4258e43 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -947,6 +947,12 @@ void main() { await room.removeSpaceChild('!1234:example.invalid');*/ }); + test('getMention', () async { + expect(room.getMention('@invalid'), null); + expect(room.getMention('@[Alice Margatroid]'), '@alice:example.org'); + expect(room.getMention('@[Alice Margatroid]#1754'), '@alice:example.org'); + }); + test('logout', () async { await matrix.logout(); }); diff --git a/test/user_test.dart b/test/user_test.dart index 2e32c01e..23c46fb3 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -159,6 +159,10 @@ void main() { expect(user1.mention, '@Bob#1745'); user1.content['displayname'] = 'Alice M'; }); + test('mentionFragments', () async { + expect(user1.mentionFragments, {'@[Alice M]', '@[Alice M]#1745'}); + expect(user2.mentionFragments, {'@Bob', '@Bob#1542'}); + }); test('dispose client', () async { await client.dispose(closeDatabase: true); });