From 024e0de4b941c2c31d9406aaf3b15c1fdbaca0cd Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 29 Aug 2021 11:43:46 +0200 Subject: [PATCH] fix: Don't lag when sending messages in big rooms The old mentionMap was very inefficient to build and scaled badly with room member size. This resulted in noticable lag when sending any message in a large room, no matter if it contained a message or not. Now, the algorithm is severly optimized and mentions (and emotes) are only loaded when actually used. --- lib/src/room.dart | 13 ++++++++----- lib/src/user.dart | 17 +++++++++++++++++ lib/src/utils/markdown.dart | 28 ++++++++++++++++------------ test/markdown_test.dart | 27 +++++++++++++++------------ test/room_test.dart | 6 ++++++ test/user_test.dart | 4 ++++ 6 files changed, 66 insertions(+), 29 deletions(-) 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); });