Merge branch 'main' into 'main'

Implement Thread creating/sending + added neccessary tests

Closes #351

See merge request famedly/company/frontend/famedlysdk!1262
This commit is contained in:
Nicolas Werner 2023-04-17 13:15:36 +00:00
commit 7f519b5619
4 changed files with 171 additions and 6 deletions

View File

@ -32,6 +32,7 @@ abstract class RelationshipTypes {
static const String reply = 'm.in_reply_to';
static const String edit = 'm.replace';
static const String reaction = 'm.annotation';
static const String thread = 'm.thread';
}
/// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event.
@ -45,6 +46,7 @@ class Event extends MatrixEvent {
@Deprecated(
'Use eventSender instead or senderFromMemoryOrFallback for a synchronous alternative')
User get sender => senderFromMemoryOrFallback;
User get senderFromMemoryOrFallback =>
room.unsafeGetUserFromMemoryOrFallback(senderId);
@ -72,6 +74,7 @@ class Event extends MatrixEvent {
: null;
MatrixEvent? _originalSource;
MatrixEvent? get originalSource => _originalSource;
Event({
@ -789,6 +792,14 @@ class Event extends MatrixEvent {
if (content.tryGet<Map<String, dynamic>>('m.relates_to') == null) {
return null;
}
if (content['m.relates_to'].containsKey('rel_type')) {
if (content
.tryGet<Map<String, dynamic>>('m.relates_to')
?.tryGet<String>('rel_type') ==
RelationshipTypes.thread) {
return RelationshipTypes.thread;
}
}
if (content['m.relates_to'].containsKey('m.in_reply_to')) {
return RelationshipTypes.reply;
}

View File

@ -633,10 +633,16 @@ class Room {
String? editEventId,
bool parseMarkdown = true,
bool parseCommands = true,
String msgtype = MessageTypes.Text}) {
String msgtype = MessageTypes.Text,
String? threadRootEventId,
String? threadLastEventId}) {
if (parseCommands) {
return client.parseAndRunCommand(this, message,
inReplyTo: inReplyTo, editEventId: editEventId, txid: txid);
inReplyTo: inReplyTo,
editEventId: editEventId,
txid: txid,
threadRootEventId: threadRootEventId,
threadLastEventId: threadLastEventId);
}
final event = <String, dynamic>{
'msgtype': msgtype,
@ -654,7 +660,11 @@ class Room {
}
}
return sendEvent(event,
txid: txid, inReplyTo: inReplyTo, editEventId: editEventId);
txid: txid,
inReplyTo: inReplyTo,
editEventId: editEventId,
threadRootEventId: threadRootEventId,
threadLastEventId: threadLastEventId);
}
/// Sends a reaction to an event with an [eventId] and the content [key] into a room.
@ -702,6 +712,8 @@ class Room {
int? shrinkImageMaxDimension,
MatrixImageFile? thumbnail,
Map<String, dynamic>? extraContent,
String? threadRootEventId,
String? threadLastEventId,
}) async {
txid ??= client.generateUniqueTransactionId();
sendingFilePlaceholders[txid] = file;
@ -895,6 +907,8 @@ class Room {
txid: txid,
inReplyTo: inReplyTo,
editEventId: editEventId,
threadRootEventId: threadRootEventId,
threadLastEventId: threadLastEventId,
);
sendingFilePlaceholders.remove(txid);
sendingFileThumbnails.remove(txid);
@ -972,6 +986,8 @@ class Room {
String? txid,
Event? inReplyTo,
String? editEventId,
String? threadRootEventId,
String? threadLastEventId,
}) async {
// Create new transaction id
final String messageID;
@ -1010,6 +1026,25 @@ class Room {
},
};
}
if (threadRootEventId != null) {
content['m.relates_to'] = {
'event_id': threadRootEventId,
'rel_type': RelationshipTypes.thread,
'is_falling_back': inReplyTo == null,
if (inReplyTo != null) ...{
'm.in_reply_to': {
'event_id': inReplyTo.eventId,
},
} else ...{
if (threadLastEventId != null)
'm.in_reply_to': {
'event_id': threadLastEventId,
},
}
};
}
if (editEventId != null) {
final newContent = content.copy();
content['m.new_content'] = newContent;

View File

@ -30,14 +30,23 @@ extension CommandsClientExtension on Client {
/// Parse and execute a string, `msg` is the input. Optionally `inReplyTo` is the event being
/// replied to and `editEventId` is the eventId of the event being replied to
Future<String?> parseAndRunCommand(Room room, String msg,
{Event? inReplyTo, String? editEventId, String? txid}) async {
Future<String?> parseAndRunCommand(
Room room,
String msg, {
Event? inReplyTo,
String? editEventId,
String? txid,
String? threadRootEventId,
String? threadLastEventId,
}) async {
final args = CommandArgs(
inReplyTo: inReplyTo,
editEventId: editEventId,
msg: '',
room: room,
txid: txid,
threadRootEventId: threadRootEventId,
threadLastEventId: threadLastEventId,
);
if (!msg.startsWith('/')) {
final sendCommand = commands['send'];
@ -86,6 +95,8 @@ extension CommandsClientExtension on Client {
editEventId: args.editEventId,
parseCommands: false,
txid: args.txid,
threadRootEventId: args.threadRootEventId,
threadLastEventId: args.threadLastEventId,
);
});
addCommand('me', (CommandArgs args) async {
@ -96,6 +107,8 @@ extension CommandsClientExtension on Client {
msgtype: MessageTypes.Emote,
parseCommands: false,
txid: args.txid,
threadRootEventId: args.threadRootEventId,
threadLastEventId: args.threadLastEventId,
);
});
addCommand('dm', (CommandArgs args) async {
@ -119,6 +132,8 @@ extension CommandsClientExtension on Client {
parseMarkdown: false,
parseCommands: false,
txid: args.txid,
threadRootEventId: args.threadRootEventId,
threadLastEventId: args.threadLastEventId,
);
});
addCommand('html', (CommandArgs args) async {
@ -270,11 +285,15 @@ class CommandArgs {
Event? inReplyTo;
Room room;
String? txid;
String? threadRootEventId;
String? threadLastEventId;
CommandArgs(
{required this.msg,
this.editEventId,
this.inReplyTo,
required this.room,
this.txid});
this.txid,
this.threadRootEventId,
this.threadLastEventId});
}

View File

@ -17,6 +17,7 @@
*/
import 'dart:convert';
import 'dart:typed_data';
import 'package:olm/olm.dart' as olm;
import 'package:test/test.dart';
@ -164,6 +165,105 @@ void main() {
});
});
test('thread', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent(
'thread',
threadRootEventId: '\$parent_event',
threadLastEventId: '\$parent_event',
);
final sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': 'thread',
'm.relates_to': {
'rel_type': 'm.thread',
'event_id': '\$parent_event',
'is_falling_back': true,
'm.in_reply_to': {'event_id': '\$parent_event'}
},
});
});
test('thread_image', () async {
FakeMatrixApi.calledEndpoints.clear();
final testImage = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg');
await room.sendFileEvent(
testImage,
threadRootEventId: '\$parent_event',
threadLastEventId: '\$parent_event',
);
final sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.image',
'body': 'file.jpeg',
'filename': 'file.jpeg',
'url': 'mxc://example.com/AQwafuaFswefuhsfAFAgsw',
'info': {
'mimetype': 'image/jpeg',
'size': 0,
},
'm.relates_to': {
'rel_type': 'm.thread',
'event_id': '\$parent_event',
'is_falling_back': true,
'm.in_reply_to': {'event_id': '\$parent_event'}
},
});
});
test('thread_reply', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('reply',
inReplyTo: Event(
eventId: '\$parent_event',
type: 'm.room.message',
content: {
'msgtype': 'm.text',
'body': 'reply',
},
originServerTs: DateTime.now(),
senderId: client.userID!,
room: room,
),
threadRootEventId: '\$parent_event',
threadLastEventId: '\$parent_event');
final sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': '> <@test:fakeServer.notExisting> reply\n\nreply',
'format': 'org.matrix.custom.html',
'formatted_body':
'<mx-reply><blockquote><a href="https://matrix.to/#/!1234:fakeServer.notExisting/\$parent_event">In reply to</a> <a href="https://matrix.to/#/@test:fakeServer.notExisting">@test:fakeServer.notExisting</a><br>reply</blockquote></mx-reply>reply',
'm.relates_to': {
'rel_type': 'm.thread',
'event_id': '\$parent_event',
'is_falling_back': false,
'm.in_reply_to': {'event_id': '\$parent_event'}
},
});
});
test('thread_different_event_ids', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent(
'thread',
threadRootEventId: '\$parent_event',
threadLastEventId: '\$last_event',
);
final sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': 'thread',
'm.relates_to': {
'rel_type': 'm.thread',
'event_id': '\$parent_event',
'is_falling_back': true,
'm.in_reply_to': {'event_id': '\$last_event'}
},
});
});
test('join', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/join !newroom:example.com');