Merge branch 'soru/event-aggregation' into 'master'
Properly imlement event aggregations See merge request famedly/famedlysdk!399
This commit is contained in:
commit
1cd430ab3b
|
@ -19,6 +19,7 @@
|
|||
abstract class EventTypes {
|
||||
static const String Message = 'm.room.message';
|
||||
static const String Sticker = 'm.sticker';
|
||||
static const String Reaction = 'm.reaction';
|
||||
static const String Redaction = 'm.room.redaction';
|
||||
static const String RoomAliases = 'm.room.aliases';
|
||||
static const String RoomCanonicalAlias = 'm.room.canonical_alias';
|
||||
|
|
|
@ -25,7 +25,6 @@ abstract class MessageTypes {
|
|||
static const String Audio = 'm.audio';
|
||||
static const String File = 'm.file';
|
||||
static const String Location = 'm.location';
|
||||
static const String Reply = 'm.relates_to';
|
||||
static const String Sticker = 'm.sticker';
|
||||
static const String BadEncrypted = 'm.bad.encrypted';
|
||||
static const String None = 'm.none';
|
||||
|
|
|
@ -28,6 +28,12 @@ import './room.dart';
|
|||
import 'utils/matrix_localizations.dart';
|
||||
import './database/database.dart' show DbRoomState, DbEvent;
|
||||
|
||||
abstract class RelationshipTypes {
|
||||
static const String Reply = 'm.in_reply_to';
|
||||
static const String Edit = 'm.replace';
|
||||
static const String Reaction = 'm.annotation';
|
||||
}
|
||||
|
||||
/// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event.
|
||||
class Event extends MatrixEvent {
|
||||
User get sender => room.getUserByMXIDSync(senderId ?? '@unknown');
|
||||
|
@ -212,10 +218,7 @@ class Event extends MatrixEvent {
|
|||
unsigned: unsigned,
|
||||
room: room);
|
||||
|
||||
String get messageType => (content['m.relates_to'] is Map &&
|
||||
content['m.relates_to']['m.in_reply_to'] != null)
|
||||
? MessageTypes.Reply
|
||||
: content['msgtype'] ?? MessageTypes.Text;
|
||||
String get messageType => content['msgtype'] ?? MessageTypes.Text;
|
||||
|
||||
void setRedactionEvent(Event redactedBecause) {
|
||||
unsigned = {
|
||||
|
@ -328,20 +331,10 @@ class Event extends MatrixEvent {
|
|||
Future<dynamic> redact({String reason, String txid}) =>
|
||||
room.redactEvent(eventId, reason: reason, txid: txid);
|
||||
|
||||
/// Whether this event is in reply to another event.
|
||||
bool get isReply =>
|
||||
content['m.relates_to'] is Map<String, dynamic> &&
|
||||
content['m.relates_to']['m.in_reply_to'] is Map<String, dynamic> &&
|
||||
content['m.relates_to']['m.in_reply_to']['event_id'] is String &&
|
||||
(content['m.relates_to']['m.in_reply_to']['event_id'] as String)
|
||||
.isNotEmpty;
|
||||
|
||||
/// Searches for the reply event in the given timeline.
|
||||
Future<Event> getReplyEvent(Timeline timeline) async {
|
||||
if (!isReply) return null;
|
||||
final String replyEventId =
|
||||
content['m.relates_to']['m.in_reply_to']['event_id'];
|
||||
return await timeline.getEventById(replyEventId);
|
||||
if (relationshipType != RelationshipTypes.Reply) return null;
|
||||
return await timeline.getEventById(relationshipEventId);
|
||||
}
|
||||
|
||||
/// If this event is encrypted and the decryption was not successful because
|
||||
|
@ -634,7 +627,6 @@ class Event extends MatrixEvent {
|
|||
case MessageTypes.Text:
|
||||
case MessageTypes.Notice:
|
||||
case MessageTypes.None:
|
||||
case MessageTypes.Reply:
|
||||
localizedBody = body;
|
||||
break;
|
||||
}
|
||||
|
@ -663,9 +655,61 @@ class Event extends MatrixEvent {
|
|||
|
||||
static const Set<String> textOnlyMessageTypes = {
|
||||
MessageTypes.Text,
|
||||
MessageTypes.Reply,
|
||||
MessageTypes.Notice,
|
||||
MessageTypes.Emote,
|
||||
MessageTypes.None,
|
||||
};
|
||||
|
||||
/// returns if this event matches the passed event or transaction id
|
||||
bool matchesEventOrTransactionId(String search) {
|
||||
if (search == null) {
|
||||
return false;
|
||||
}
|
||||
if (eventId == search) {
|
||||
return true;
|
||||
}
|
||||
return unsigned != null && unsigned['transaction_id'] == search;
|
||||
}
|
||||
|
||||
/// Get the relationship type of an event. `null` if there is none
|
||||
String get relationshipType {
|
||||
if (content == null || !(content['m.relates_to'] is Map)) {
|
||||
return null;
|
||||
}
|
||||
if (content['m.relates_to'].containsKey('rel_type')) {
|
||||
return content['m.relates_to']['rel_type'];
|
||||
}
|
||||
if (content['m.relates_to'].containsKey('m.in_reply_to')) {
|
||||
return RelationshipTypes.Reply;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get the event ID that this relationship will reference. `null` if there is none
|
||||
String get relationshipEventId {
|
||||
if (content == null || !(content['m.relates_to'] is Map)) {
|
||||
return null;
|
||||
}
|
||||
if (content['m.relates_to'].containsKey('event_id')) {
|
||||
return content['m.relates_to']['event_id'];
|
||||
}
|
||||
if (content['m.relates_to']['m.in_reply_to'] is Map &&
|
||||
content['m.relates_to']['m.in_reply_to'].containsKey('event_id')) {
|
||||
return content['m.relates_to']['m.in_reply_to']['event_id'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get wether this event has aggregated events from a certain [type]
|
||||
/// To be able to do that you need to pass a [timeline]
|
||||
bool hasAggregatedEvents(Timeline timeline, String type) =>
|
||||
timeline.aggregatedEvents.containsKey(eventId) &&
|
||||
timeline.aggregatedEvents[eventId].containsKey(type);
|
||||
|
||||
/// Get all the aggregated event objects for a given [type]. To be able to do this
|
||||
/// you have to pass a [timeline]
|
||||
Set<Event> aggregatedEvents(Timeline timeline, String type) =>
|
||||
hasAggregatedEvents(timeline, type)
|
||||
? timeline.aggregatedEvents[eventId][type]
|
||||
: <Event>{};
|
||||
}
|
||||
|
|
|
@ -104,7 +104,9 @@ class Room {
|
|||
/// Flag if the room is partial, meaning not all state events have been loaded yet
|
||||
bool partial = true;
|
||||
|
||||
/// Load all the missing state events for the room from the database. If the room has already been loaded, this does nothing.
|
||||
/// Post-loads the room.
|
||||
/// This load all the missing state events for the room from the database
|
||||
/// If the room has already been loaded, this does nothing.
|
||||
Future<void> postLoad() async {
|
||||
if (!partial || client.database == null) {
|
||||
return;
|
||||
|
@ -500,6 +502,7 @@ class Room {
|
|||
Future<String> sendTextEvent(String message,
|
||||
{String txid,
|
||||
Event inReplyTo,
|
||||
String editEventId,
|
||||
bool parseMarkdown = true,
|
||||
Map<String, Map<String, String>> emotePacks}) {
|
||||
final event = <String, dynamic>{
|
||||
|
@ -518,7 +521,20 @@ class Room {
|
|||
event['formatted_body'] = html;
|
||||
}
|
||||
}
|
||||
return sendEvent(event, txid: txid, inReplyTo: inReplyTo);
|
||||
return sendEvent(event,
|
||||
txid: txid, inReplyTo: inReplyTo, editEventId: editEventId);
|
||||
}
|
||||
|
||||
/// Sends a reaction to an event with an [eventId] and the content [key] into a room.
|
||||
/// Returns the event ID generated by the server for this reaction.
|
||||
Future<String> sendReaction(String eventId, String key, {String txid}) {
|
||||
return sendEvent({
|
||||
'm.relates_to': {
|
||||
'rel_type': RelationshipTypes.Reaction,
|
||||
'event_id': eventId,
|
||||
'key': key,
|
||||
},
|
||||
}, type: EventTypes.Reaction, txid: txid);
|
||||
}
|
||||
|
||||
/// Sends a [file] to this room after uploading it. Returns the mxc uri of
|
||||
|
@ -529,6 +545,7 @@ class Room {
|
|||
MatrixFile file, {
|
||||
String txid,
|
||||
Event inReplyTo,
|
||||
String editEventId,
|
||||
bool waitUntilSent = false,
|
||||
MatrixImageFile thumbnail,
|
||||
}) async {
|
||||
|
@ -605,6 +622,7 @@ class Room {
|
|||
content,
|
||||
txid: txid,
|
||||
inReplyTo: inReplyTo,
|
||||
editEventId: editEventId,
|
||||
);
|
||||
if (waitUntilSent) {
|
||||
await sendResponse;
|
||||
|
@ -615,7 +633,7 @@ class Room {
|
|||
/// Sends an event to this room with this json as a content. Returns the
|
||||
/// event ID generated from the server.
|
||||
Future<String> sendEvent(Map<String, dynamic> content,
|
||||
{String type, String txid, Event inReplyTo}) async {
|
||||
{String type, String txid, Event inReplyTo, String editEventId}) async {
|
||||
type = type ?? EventTypes.Message;
|
||||
final sendType =
|
||||
(encrypted && client.encryptionEnabled) ? EventTypes.Encrypted : type;
|
||||
|
@ -645,6 +663,20 @@ class Room {
|
|||
},
|
||||
};
|
||||
}
|
||||
if (editEventId != null) {
|
||||
final newContent = Map<String, dynamic>.from(content);
|
||||
content['m.new_content'] = newContent;
|
||||
content['m.relates_to'] = {
|
||||
'event_id': editEventId,
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
};
|
||||
if (content['body'] is String) {
|
||||
content['body'] = '* ' + content['body'];
|
||||
}
|
||||
if (content['formatted_body'] is String) {
|
||||
content['formatted_body'] = '* ' + content['formatted_body'];
|
||||
}
|
||||
}
|
||||
|
||||
final sortOrder = newSortOrder;
|
||||
// Display a *sending* event and store it.
|
||||
|
|
|
@ -35,6 +35,9 @@ class Timeline {
|
|||
final Room room;
|
||||
List<Event> events = [];
|
||||
|
||||
/// Map of event ID to map of type to set of aggregated events
|
||||
Map<String, Map<String, Set<Event>>> aggregatedEvents = {};
|
||||
|
||||
final onTimelineUpdateCallback onUpdate;
|
||||
final onTimelineInsertCallback onInsert;
|
||||
|
||||
|
@ -66,7 +69,10 @@ class Timeline {
|
|||
await room.requestHistory(
|
||||
historyCount: historyCount,
|
||||
onHistoryReceived: () {
|
||||
if (room.prev_batch.isEmpty || room.prev_batch == null) events = [];
|
||||
if (room.prev_batch.isEmpty || room.prev_batch == null) {
|
||||
events.clear();
|
||||
aggregatedEvents.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
@ -82,9 +88,17 @@ class Timeline {
|
|||
// to be received via the onEvent stream, it is unneeded to call sortAndUpdate
|
||||
roomSub ??= room.client.onRoomUpdate.stream
|
||||
.where((r) => r.id == room.id && r.limitedTimeline == true)
|
||||
.listen((r) => events.clear());
|
||||
.listen((r) {
|
||||
events.clear();
|
||||
aggregatedEvents.clear();
|
||||
});
|
||||
sessionIdReceivedSub ??=
|
||||
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
||||
|
||||
// we want to populate our aggregated events
|
||||
for (final e in events) {
|
||||
addAggregatedEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Don't forget to call this before you dismiss this object!
|
||||
|
@ -151,6 +165,47 @@ class Timeline {
|
|||
return i;
|
||||
}
|
||||
|
||||
void _removeEventFromSet(Set<Event> eventSet, Event event) {
|
||||
eventSet.removeWhere((e) =>
|
||||
e.matchesEventOrTransactionId(event.eventId) ||
|
||||
(event.unsigned != null &&
|
||||
e.matchesEventOrTransactionId(event.unsigned['transaction_id'])));
|
||||
}
|
||||
|
||||
void addAggregatedEvent(Event event) {
|
||||
// we want to add an event to the aggregation tree
|
||||
if (event.relationshipType == null || event.relationshipEventId == null) {
|
||||
return; // nothing to do
|
||||
}
|
||||
if (!aggregatedEvents.containsKey(event.relationshipEventId)) {
|
||||
aggregatedEvents[event.relationshipEventId] = <String, Set<Event>>{};
|
||||
}
|
||||
if (!aggregatedEvents[event.relationshipEventId]
|
||||
.containsKey(event.relationshipType)) {
|
||||
aggregatedEvents[event.relationshipEventId]
|
||||
[event.relationshipType] = <Event>{};
|
||||
}
|
||||
// remove a potential old event
|
||||
_removeEventFromSet(
|
||||
aggregatedEvents[event.relationshipEventId][event.relationshipType],
|
||||
event);
|
||||
// add the new one
|
||||
aggregatedEvents[event.relationshipEventId][event.relationshipType]
|
||||
.add(event);
|
||||
}
|
||||
|
||||
void removeAggregatedEvent(Event event) {
|
||||
aggregatedEvents.remove(event.eventId);
|
||||
if (event.unsigned != null) {
|
||||
aggregatedEvents.remove(event.unsigned['transaction_id']);
|
||||
}
|
||||
for (final types in aggregatedEvents.values) {
|
||||
for (final events in types.values) {
|
||||
_removeEventFromSet(events, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleEventUpdate(EventUpdate eventUpdate) async {
|
||||
try {
|
||||
if (eventUpdate.roomID != room.id) return;
|
||||
|
@ -161,12 +216,16 @@ class Timeline {
|
|||
if (eventUpdate.eventType == EventTypes.Redaction) {
|
||||
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
|
||||
if (eventId != null) {
|
||||
removeAggregatedEvent(events[eventId]);
|
||||
events[eventId].setRedactionEvent(Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder));
|
||||
}
|
||||
} else if (status == -2) {
|
||||
var i = _findEvent(event_id: eventUpdate.content['event_id']);
|
||||
if (i < events.length) events.removeAt(i);
|
||||
if (i < events.length) {
|
||||
removeAggregatedEvent(events[i]);
|
||||
events.removeAt(i);
|
||||
}
|
||||
} else {
|
||||
var i = _findEvent(
|
||||
event_id: eventUpdate.content['event_id'],
|
||||
|
@ -186,6 +245,7 @@ class Timeline {
|
|||
if (status < oldStatus && !(status == -1 && oldStatus == 0)) {
|
||||
events[i].status = oldStatus;
|
||||
}
|
||||
addAggregatedEvent(events[i]);
|
||||
} else {
|
||||
var newEvent = Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
|
@ -196,6 +256,7 @@ class Timeline {
|
|||
-1) return;
|
||||
|
||||
events.insert(0, newEvent);
|
||||
addAggregatedEvent(newEvent);
|
||||
if (onInsert != null) onInsert(0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ void main() {
|
|||
expect(event.formattedText, formatted_body);
|
||||
expect(event.body, body);
|
||||
expect(event.type, EventTypes.Message);
|
||||
expect(event.isReply, true);
|
||||
expect(event.relationshipType, RelationshipTypes.Reply);
|
||||
jsonObj['state_key'] = '';
|
||||
var state = Event.fromJson(jsonObj, null);
|
||||
expect(state.eventId, id);
|
||||
|
@ -160,7 +160,43 @@ void main() {
|
|||
'event_id': '1234',
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.messageType, MessageTypes.Reply);
|
||||
expect(event.messageType, MessageTypes.Text);
|
||||
expect(event.relationshipType, RelationshipTypes.Reply);
|
||||
expect(event.relationshipEventId, '1234');
|
||||
});
|
||||
|
||||
test('relationship types', () async {
|
||||
Event event;
|
||||
|
||||
jsonObj['content'] = <String, dynamic>{
|
||||
'msgtype': 'm.text',
|
||||
'text': 'beep',
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, null);
|
||||
expect(event.relationshipEventId, null);
|
||||
|
||||
jsonObj['content']['m.relates_to'] = <String, dynamic>{
|
||||
'rel_type': 'm.replace',
|
||||
'event_id': 'abc',
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, RelationshipTypes.Edit);
|
||||
expect(event.relationshipEventId, 'abc');
|
||||
|
||||
jsonObj['content']['m.relates_to']['rel_type'] = 'm.annotation';
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, RelationshipTypes.Reaction);
|
||||
expect(event.relationshipEventId, 'abc');
|
||||
|
||||
jsonObj['content']['m.relates_to'] = {
|
||||
'm.in_reply_to': {
|
||||
'event_id': 'def',
|
||||
},
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, RelationshipTypes.Reply);
|
||||
expect(event.relationshipEventId, 'def');
|
||||
});
|
||||
|
||||
test('redact', () async {
|
||||
|
@ -790,5 +826,56 @@ void main() {
|
|||
}, room);
|
||||
expect(event.getLocalizedBody(FakeMatrixLocalizations()), null);
|
||||
});
|
||||
|
||||
test('aggregations', () {
|
||||
var event = Event.fromJson({
|
||||
'content': {
|
||||
'body': 'blah',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'event_id': '\$source',
|
||||
}, null);
|
||||
var edit1 = Event.fromJson({
|
||||
'content': {
|
||||
'body': 'blah',
|
||||
'msgtype': 'm.text',
|
||||
'm.relates_to': {
|
||||
'event_id': '\$source',
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit1',
|
||||
}, null);
|
||||
var edit2 = Event.fromJson({
|
||||
'content': {
|
||||
'body': 'blah',
|
||||
'msgtype': 'm.text',
|
||||
'm.relates_to': {
|
||||
'event_id': '\$source',
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
}, null);
|
||||
var room = Room(client: client);
|
||||
var timeline = Timeline(events: <Event>[event, edit1, edit2], room: room);
|
||||
expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Edit), true);
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit),
|
||||
{edit1, edit2});
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Reaction),
|
||||
<Event>{});
|
||||
expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Reaction),
|
||||
false);
|
||||
|
||||
timeline.removeAggregatedEvent(edit2);
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit), {edit1});
|
||||
timeline.addAggregatedEvent(edit2);
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit),
|
||||
{edit1, edit2});
|
||||
|
||||
timeline.removeAggregatedEvent(event);
|
||||
expect(
|
||||
event.aggregatedEvents(timeline, RelationshipTypes.Edit), <Event>{});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2010,6 +2010,10 @@ class FakeMatrixApi extends MockClient {
|
|||
(var reqI) => {
|
||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||
},
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.reaction/testtxid':
|
||||
(var reqI) => {
|
||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||
},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/!1234%3Aexample.com/send/m.room.message/1234':
|
||||
|
|
|
@ -27,7 +27,9 @@ import 'package:famedlysdk/src/database/database.dart'
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'fake_client.dart';
|
||||
import 'fake_matrix_api.dart';
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
void main() {
|
||||
|
@ -349,9 +351,87 @@ void main() {
|
|||
});
|
||||
|
||||
test('sendEvent', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp =
|
||||
await room.sendTextEvent('Hello world', 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, {
|
||||
'body': 'Hello world',
|
||||
'msgtype': 'm.text',
|
||||
});
|
||||
});
|
||||
|
||||
test('send edit', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp = await room.sendTextEvent('Hello world',
|
||||
txid: 'testtxid', editEventId: '\$otherEvent');
|
||||
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, {
|
||||
'body': '* Hello world',
|
||||
'msgtype': 'm.text',
|
||||
'm.new_content': {
|
||||
'body': 'Hello world',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'm.relates_to': {
|
||||
'event_id': '\$otherEvent',
|
||||
'rel_type': 'm.replace',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('send reply', () async {
|
||||
var event = Event.fromJson({
|
||||
'event_id': '\$replyEvent',
|
||||
'content': {
|
||||
'body': 'Blah',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'type': 'm.room.message',
|
||||
'sender': '@alice:example.org',
|
||||
}, room);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp = await room.sendTextEvent('Hello world',
|
||||
txid: 'testtxid', inReplyTo: event);
|
||||
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, {
|
||||
'body': '> <@alice:example.org> Blah\n\nHello world',
|
||||
'msgtype': 'm.text',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br>Blah</blockquote></mx-reply>Hello world',
|
||||
'm.relates_to': {
|
||||
'm.in_reply_to': {
|
||||
'event_id': '\$replyEvent',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('send reaction', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp =
|
||||
await room.sendReaction('\$otherEvent', '🦊', txid: 'testtxid');
|
||||
expect(resp.startsWith('\$event'), true);
|
||||
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||
.firstWhere((p) => p.key.contains('/send/m.reaction/'));
|
||||
final content = json.decode(entry.value.first);
|
||||
expect(content, {
|
||||
'm.relates_to': {
|
||||
'event_id': '\$otherEvent',
|
||||
'rel_type': 'm.annotation',
|
||||
'key': '🦊',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Not working because there is no real file to test it...
|
||||
|
|
Loading…
Reference in a new issue