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.
This commit is contained in:
Sorunome 2021-08-29 11:43:46 +02:00
parent aeea0669d5
commit 024e0de4b9
No known key found for this signature in database
GPG Key ID: B19471D07FC9BE9C
6 changed files with 66 additions and 29 deletions

View File

@ -566,6 +566,12 @@ class Room {
Map<String, Map<String, String>> get emotePacks => Map<String, Map<String, String>> get emotePacks =>
getImagePacksFlat(ImagePackUsage.emoticon); 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 /// Sends a normal text message to this room. Returns the event ID generated
/// by the server for this message. /// by the server for this message.
Future<String> sendTextEvent(String message, Future<String> sendTextEvent(String message,
@ -585,12 +591,9 @@ class Room {
'body': message, 'body': message,
}; };
if (parseMarkdown) { if (parseMarkdown) {
final mentionMap = <String, String>{
for (final user in getParticipants()) user.mention: user.id
};
final html = markdown(event['body'], final html = markdown(event['body'],
emotePacks: getImagePacksFlat(ImagePackUsage.emoticon), getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon),
mentionMap: mentionMap); getMention: getMention);
// if the decoded html is the same as the body, there is no need in sending a formatted message // 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'<br />\n?'), '\n')) != if (HtmlUnescape().convert(html.replaceAll(RegExp(r'<br />\n?'), '\n')) !=
event['body']) { event['body']) {

View File

@ -207,6 +207,23 @@ class User extends Event {
} }
return '$identifier#$ourHash'; return '$identifier#$ourHash';
} }
/// Get the mention fragments for this user.
Set<String> 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) { String _hash(String s) {

View File

@ -72,11 +72,13 @@ class SpoilerSyntax extends TagSyntax {
} }
class EmoteSyntax extends InlineSyntax { class EmoteSyntax extends InlineSyntax {
final Map<String, Map<String, String>> emotePacks; final Map<String, Map<String, String>> Function() getEmotePacks;
EmoteSyntax(this.emotePacks) : super(r':(?:([-\w]+)~)?([-\w]+):'); Map<String, Map<String, String>> emotePacks;
EmoteSyntax(this.getEmotePacks) : super(r':(?:([-\w]+)~)?([-\w]+):');
@override @override
bool onMatch(InlineParser parser, Match match) { bool onMatch(InlineParser parser, Match match) {
emotePacks ??= getEmotePacks?.call() ?? <String, Map<String, String>>{};
final pack = match[1] ?? ''; final pack = match[1] ?? '';
final emote = match[2]; final emote = match[2];
String mxc; String mxc;
@ -182,29 +184,31 @@ class PillSyntax extends InlineSyntax {
} }
class MentionSyntax extends InlineSyntax { class MentionSyntax extends InlineSyntax {
final Map<String, String> mentionMap; final String Function(String) getMention;
MentionSyntax(this.mentionMap) : super(r'(@(?:\[[^\]:]+\]|\w+)(?:#\w+)?)'); MentionSyntax(this.getMention) : super(r'(@(?:\[[^\]:]+\]|\w+)(?:#\w+)?)');
@override @override
bool onMatch(InlineParser parser, Match match) { bool onMatch(InlineParser parser, Match match) {
final mention = getMention?.call(match[1]);
if ((match.start > 0 && if ((match.start > 0 &&
!RegExp(r'[\s.!?:;\(]').hasMatch(match.input[match.start - 1])) || !RegExp(r'[\s.!?:;\(]').hasMatch(match.input[match.start - 1])) ||
!mentionMap.containsKey(match[1])) { mention == null) {
parser.addNode(Text(match[0])); parser.addNode(Text(match[0]));
return true; return true;
} }
final identifier = mentionMap[match[1]];
final element = Element.text('a', htmlEscape.convert(match[1])); final element = Element.text('a', htmlEscape.convert(match[1]));
element.attributes['href'] = element.attributes['href'] =
htmlAttrEscape.convert('https://matrix.to/#/$identifier'); htmlAttrEscape.convert('https://matrix.to/#/$mention');
parser.addNode(element); parser.addNode(element);
return true; return true;
} }
} }
String markdown(String text, String markdown(
{Map<String, Map<String, String>> emotePacks, String text, {
Map<String, String> mentionMap}) { Map<String, Map<String, String>> Function() getEmotePacks,
String Function(String) getMention,
}) {
var ret = markdownToHtml( var ret = markdownToHtml(
text, text,
extensionSet: ExtensionSet.commonMark, extensionSet: ExtensionSet.commonMark,
@ -215,9 +219,9 @@ String markdown(String text,
StrikethroughSyntax(), StrikethroughSyntax(),
LinebreakSyntax(), LinebreakSyntax(),
SpoilerSyntax(), SpoilerSyntax(),
EmoteSyntax(emotePacks ?? <String, Map<String, String>>{}), EmoteSyntax(getEmotePacks),
PillSyntax(), PillSyntax(),
MentionSyntax(mentionMap ?? <String, String>{}), MentionSyntax(getMention),
InlineLatexSyntax(), InlineLatexSyntax(),
], ],
); );

View File

@ -40,6 +40,7 @@ void main() {
'@[Fast Fox]#123': '@fastfox:example.org', '@[Fast Fox]#123': '@fastfox:example.org',
'@[">]': '@blah:example.org', '@[">]': '@blah:example.org',
}; };
final getMention = (mention) => mentionMap[mention];
test('simple markdown', () { test('simple markdown', () {
expect(markdown('hey *there* how are **you** doing?'), expect(markdown('hey *there* how are **you** doing?'),
'hey <em>there</em> how are <strong>you</strong> doing?'); 'hey <em>there</em> how are <strong>you</strong> doing?');
@ -67,16 +68,18 @@ void main() {
expect(markdown('foxies\ncute'), 'foxies<br />\ncute'); expect(markdown('foxies\ncute'), 'foxies<br />\ncute');
}); });
test('emotes', () { test('emotes', () {
expect(markdown(':fox:', emotePacks: emotePacks), expect(markdown(':fox:', getEmotePacks: () => emotePacks),
'<img data-mx-emoticon="" src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />'); '<img data-mx-emoticon="" src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':user~fox:', emotePacks: emotePacks), expect(markdown(':user~fox:', getEmotePacks: () => emotePacks),
'<img data-mx-emoticon="" src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />'); '<img data-mx-emoticon="" src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':raccoon:', emotePacks: emotePacks), expect(markdown(':raccoon:', getEmotePacks: () => emotePacks),
'<img data-mx-emoticon="" src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />'); '<img data-mx-emoticon="" src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
expect(markdown(':invalid:', emotePacks: emotePacks), ':invalid:');
expect(markdown(':invalid:?!', emotePacks: emotePacks), ':invalid:?!');
expect( 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', () { test('pills', () {
expect(markdown('Hey @sorunome:sorunome.de!'), expect(markdown('Hey @sorunome:sorunome.de!'),
@ -95,17 +98,17 @@ void main() {
'Hey <a href="https://matrix.to/#/@sorunome:[::1]">@sorunome:[::1]</a>!'); 'Hey <a href="https://matrix.to/#/@sorunome:[::1]">@sorunome:[::1]</a>!');
}); });
test('mentions', () { test('mentions', () {
expect(markdown('Hey @Bob!', mentionMap: mentionMap), expect(markdown('Hey @Bob!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@bob:example.org">@Bob</a>!'); 'Hey <a href="https://matrix.to/#/@bob:example.org">@Bob</a>!');
expect(markdown('How is @[Bob Ross] doing?', mentionMap: mentionMap), expect(markdown('How is @[Bob Ross] doing?', getMention: getMention),
'How is <a href="https://matrix.to/#/@bobross:example.org">@[Bob Ross]</a> doing?'); 'How is <a href="https://matrix.to/#/@bobross:example.org">@[Bob Ross]</a> doing?');
expect( expect(
markdown('Hey @invalid!', mentionMap: mentionMap), 'Hey @invalid!'); markdown('Hey @invalid!', getMention: getMention), 'Hey @invalid!');
expect(markdown('Hey @Fox#123!', mentionMap: mentionMap), expect(markdown('Hey @Fox#123!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@fox:example.org">@Fox#123</a>!'); 'Hey <a href="https://matrix.to/#/@fox:example.org">@Fox#123</a>!');
expect(markdown('Hey @[Fast Fox]#123!', mentionMap: mentionMap), expect(markdown('Hey @[Fast Fox]#123!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@fastfox:example.org">@[Fast Fox]#123</a>!'); 'Hey <a href="https://matrix.to/#/@fastfox:example.org">@[Fast Fox]#123</a>!');
expect(markdown('Hey @[">]!', mentionMap: mentionMap), expect(markdown('Hey @[">]!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@blah:example.org">@[&quot;&gt;]</a>!'); 'Hey <a href="https://matrix.to/#/@blah:example.org">@[&quot;&gt;]</a>!');
}); });
test('latex', () { test('latex', () {

View File

@ -947,6 +947,12 @@ void main() {
await room.removeSpaceChild('!1234:example.invalid');*/ 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 { test('logout', () async {
await matrix.logout(); await matrix.logout();
}); });

View File

@ -159,6 +159,10 @@ void main() {
expect(user1.mention, '@Bob#1745'); expect(user1.mention, '@Bob#1745');
user1.content['displayname'] = 'Alice M'; 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 { test('dispose client', () async {
await client.dispose(closeDatabase: true); await client.dispose(closeDatabase: true);
}); });