diff --git a/lib/src/event.dart b/lib/src/event.dart index 43f4d27c..3cb2da50 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1180,6 +1180,17 @@ class Event extends MatrixEvent { (fileSendingStatus) => fileSendingStatus.name == status, ); } + + /// Returns the mentioned userIds and wether the event includes an @room + /// mention. This is only determined by the `m.mention` object in the event + /// content. + ({List userIds, bool room}) get mentions { + final mentionsMap = content.tryGetMap('m.mentions'); + return ( + userIds: mentionsMap?.tryGetList('user_ids') ?? [], + room: mentionsMap?.tryGet('room') ?? false, + ); + } } enum FileSendingStatus { diff --git a/lib/src/room.dart b/lib/src/room.dart index dd7c6825..e6e92327 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -710,6 +710,7 @@ class Room { String? threadRootEventId, String? threadLastEventId, StringBuffer? commandStdout, + bool addMentions = true, }) { if (parseCommands) { return client.parseAndRunCommand( @@ -727,6 +728,41 @@ class Room { 'msgtype': msgtype, 'body': message, }; + + if (addMentions) { + var potentialMentions = message + .split('@') + .map( + (text) => text.startsWith('[') + ? '@${text.split(']').first}]' + : '@${text.split(RegExp(r'\s+')).first}', + ) + .toList() + ..removeAt(0); + + final hasRoomMention = potentialMentions.remove('@room'); + + potentialMentions = potentialMentions + .map( + (mention) => + mention.isValidMatrixId ? mention : getMention(mention), + ) + .nonNulls + .toSet() // Deduplicate + .toList() + ..remove(client.userID); // We should never mention ourself. + + // https://spec.matrix.org/v1.7/client-server-api/#mentioning-the-replied-to-user + if (inReplyTo != null) potentialMentions.add(inReplyTo.senderId); + + if (hasRoomMention || potentialMentions.isNotEmpty) { + event['m.mentions'] = { + if (hasRoomMention) 'room': true, + if (potentialMentions.isNotEmpty) 'user_ids': potentialMentions, + }; + } + } + if (parseMarkdown) { final html = markdown( event['body'], diff --git a/test/commands_test.dart b/test/commands_test.dart index 238c00cb..6efc34e6 100644 --- a/test/commands_test.dart +++ b/test/commands_test.dart @@ -243,6 +243,9 @@ void main() { expect(sent, { 'msgtype': 'm.text', 'body': '> <@test:fakeServer.notExisting> reply\n\nreply', + 'm.mentions': { + 'user_ids': ['@test:fakeServer.notExisting'], + }, 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @test:fakeServer.notExisting
reply
reply', diff --git a/test/event_test.dart b/test/event_test.dart index 7f244095..e3edae90 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -2951,5 +2951,28 @@ void main() async { Timeline(room: room, chunk: TimelineChunk(events: [targetEvent])); expect(await event.getReplyEvent(timeline), targetEvent); }); + test('getMentions', () { + final event = Event.fromJson( + { + 'content': { + 'msgtype': 'text', + 'body': 'Hello world @alice:matrix.org', + 'm.mentions': { + 'user_ids': ['@alice:matrix.org'], + 'room': false, + }, + }, + 'event_id': '\$143273582443PhrSn:example.org', + 'origin_server_ts': 1432735824653, + 'room_id': room.id, + 'sender': '@example:example.org', + 'type': 'm.room.message', + 'unsigned': {'age': 1234}, + }, + room, + ); + expect(event.mentions.userIds, ['@alice:matrix.org']); + expect(event.mentions.room, false); + }); }); } diff --git a/test/room_test.dart b/test/room_test.dart index 01fe4d20..175b3459 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -1038,6 +1038,36 @@ void main() { }); }); + test('sendEvent with room mention', () async { + FakeMatrixApi.calledEndpoints.clear(); + final resp = await room.sendTextEvent( + 'Hello world @room', + txid: 'testtxid', + addMentions: true, + ); + expect(resp?.startsWith('\$event'), true); + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/m.room.message/')); + final content = json.decode(entry.value.first); + expect(content['m.mentions'], {'room': true}); + }); + + test('sendEvent with user mention', () async { + FakeMatrixApi.calledEndpoints.clear(); + final resp = await room.sendTextEvent( + 'Hello world @[Alice Margatroid]', + addMentions: true, + txid: 'testtxid', + ); + expect(resp?.startsWith('\$event'), true); + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/m.room.message/')); + final content = json.decode(entry.value.first); + expect(content['m.mentions'], { + 'user_ids': ['@alice:matrix.org'], + }); + }); + test('send edit', () async { FakeMatrixApi.calledEndpoints.clear(); final dynamic resp = await room.sendTextEvent( @@ -1089,6 +1119,9 @@ void main() { expect(content, { 'body': '> <@alice:example.org> Blah\n\nHello world', 'msgtype': 'm.text', + 'm.mentions': { + 'user_ids': ['@alice:example.org'], + }, 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Blah
Hello world', @@ -1125,6 +1158,9 @@ void main() { 'body': '> <@alice:example.org> Blah\n> beep\n\nHello world\nfox', 'msgtype': 'm.text', + 'm.mentions': { + 'user_ids': ['@alice:example.org'], + }, 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
<b>Blah</b>
beep
Hello world
fox', @@ -1162,6 +1198,9 @@ void main() { expect(content, { 'body': '> <@alice:example.org> plaintext meow\n\nHello world', 'msgtype': 'm.text', + 'm.mentions': { + 'user_ids': ['@alice:example.org'], + }, 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
meow
Hello world', @@ -1197,6 +1236,9 @@ void main() { expect(content, { 'body': '> <@alice:example.org> Hey @\u{200b}room\n\nHello world', 'msgtype': 'm.text', + 'm.mentions': { + 'user_ids': ['@alice:example.org'], + }, 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Hey @room
Hello world', @@ -1214,6 +1256,9 @@ void main() { 'content': { 'body': '> <@alice:example.org> Hey\n\nHello world', 'msgtype': 'm.text', + 'm.mentions': { + 'user_ids': ['@alice:example.org'], + }, 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Hey
Hello world', @@ -1238,6 +1283,9 @@ void main() { expect(content, { 'body': '> <@alice:example.org> Hello world\n\nFox', 'msgtype': 'm.text', + 'm.mentions': { + 'user_ids': ['@alice:example.org'], + }, 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Hello world
Fox', @@ -1296,7 +1344,7 @@ void main() { test('sendFileEvent', () async { final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg'); final resp = await room.sendFileEvent(testFile, txid: 'testtxid'); - expect(resp.toString(), '\$event10'); + expect(resp.toString(), '\$event12'); }); test('pushRuleState', () async {