feat: allow sending messages inside threads
This commit is contained in:
parent
0a1d4c6544
commit
203fc25d7a
|
|
@ -33,6 +33,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.
|
||||
|
|
@ -46,6 +47,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);
|
||||
|
||||
|
|
@ -73,6 +75,7 @@ class Event extends MatrixEvent {
|
|||
: null;
|
||||
|
||||
MatrixEvent? _originalSource;
|
||||
|
||||
MatrixEvent? get originalSource => _originalSource;
|
||||
|
||||
Event({
|
||||
|
|
@ -788,6 +791,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -654,10 +654,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,
|
||||
|
|
@ -675,7 +681,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.
|
||||
|
|
@ -723,6 +733,8 @@ class Room {
|
|||
int? shrinkImageMaxDimension,
|
||||
MatrixImageFile? thumbnail,
|
||||
Map<String, dynamic>? extraContent,
|
||||
String? threadRootEventId,
|
||||
String? threadLastEventId,
|
||||
}) async {
|
||||
final mediaConfig = await client.getConfig();
|
||||
final maxMediaSize = mediaConfig.mUploadSize;
|
||||
|
|
@ -906,6 +918,8 @@ class Room {
|
|||
txid: txid,
|
||||
inReplyTo: inReplyTo,
|
||||
editEventId: editEventId,
|
||||
threadRootEventId: threadRootEventId,
|
||||
threadLastEventId: threadLastEventId,
|
||||
);
|
||||
sendingFilePlaceholders.remove(txid);
|
||||
sendingFileThumbnails.remove(txid);
|
||||
|
|
@ -983,6 +997,8 @@ class Room {
|
|||
String? txid,
|
||||
Event? inReplyTo,
|
||||
String? editEventId,
|
||||
String? threadRootEventId,
|
||||
String? threadLastEventId,
|
||||
}) async {
|
||||
// Create new transaction id
|
||||
String messageID;
|
||||
|
|
@ -1021,6 +1037,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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -263,11 +278,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});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue