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:
parent
aeea0669d5
commit
024e0de4b9
|
|
@ -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']) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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">@[">]</a>!');
|
'Hey <a href="https://matrix.to/#/@blah:example.org">@[">]</a>!');
|
||||||
});
|
});
|
||||||
test('latex', () {
|
test('latex', () {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue