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 =>
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<String> sendTextEvent(String message,
@ -585,12 +591,9 @@ class Room {
'body': message,
};
if (parseMarkdown) {
final mentionMap = <String, String>{
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'<br />\n?'), '\n')) !=
event['body']) {

View File

@ -207,6 +207,23 @@ class User extends Event {
}
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) {

View File

@ -72,11 +72,13 @@ class SpoilerSyntax extends TagSyntax {
}
class EmoteSyntax extends InlineSyntax {
final Map<String, Map<String, String>> emotePacks;
EmoteSyntax(this.emotePacks) : super(r':(?:([-\w]+)~)?([-\w]+):');
final Map<String, Map<String, String>> Function() getEmotePacks;
Map<String, Map<String, String>> emotePacks;
EmoteSyntax(this.getEmotePacks) : super(r':(?:([-\w]+)~)?([-\w]+):');
@override
bool onMatch(InlineParser parser, Match match) {
emotePacks ??= getEmotePacks?.call() ?? <String, Map<String, String>>{};
final pack = match[1] ?? '';
final emote = match[2];
String mxc;
@ -182,29 +184,31 @@ class PillSyntax extends InlineSyntax {
}
class MentionSyntax extends InlineSyntax {
final Map<String, String> 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<String, Map<String, String>> emotePacks,
Map<String, String> mentionMap}) {
String markdown(
String text, {
Map<String, Map<String, String>> 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 ?? <String, Map<String, String>>{}),
EmoteSyntax(getEmotePacks),
PillSyntax(),
MentionSyntax(mentionMap ?? <String, String>{}),
MentionSyntax(getMention),
InlineLatexSyntax(),
],
);

View File

@ -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 <em>there</em> how are <strong>you</strong> doing?');
@ -67,16 +68,18 @@ void main() {
expect(markdown('foxies\ncute'), 'foxies<br />\ncute');
});
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" />');
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" />');
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" />');
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 <a href="https://matrix.to/#/@sorunome:[::1]">@sorunome:[::1]</a>!');
});
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>!');
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?');
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 <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>!');
expect(markdown('Hey @[">]!', mentionMap: mentionMap),
expect(markdown('Hey @[">]!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@blah:example.org">@[&quot;&gt;]</a>!');
});
test('latex', () {

View File

@ -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();
});

View File

@ -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);
});