refactor: Store lastEvent in room object instead of room state

This commit is contained in:
krille-chan 2024-02-28 14:50:40 +01:00 committed by Krille
parent d1cf6e2e9e
commit 20073ddd49
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652
11 changed files with 233 additions and 322 deletions

View File

@ -193,22 +193,9 @@ class KeyManager {
event.content['session_id'] == sessionId) {
final decrypted = encryption.decryptRoomEventSync(roomId, event);
if (decrypted.type != EventTypes.Encrypted) {
// Remove this from the state to make sure it does not appear as last event
room.states.remove(EventTypes.Encrypted);
// Set the decrypted event as last event by adding it to the state
room.states[decrypted.type] = {'': decrypted};
// Also store in database
final database = client.database;
if (database != null) {
await database.storeEventUpdate(
EventUpdate(
roomID: room.id,
type: EventUpdateType.state,
content: decrypted.toJson(),
),
client,
);
}
// No need to persist it as the lastEvent is persisted in the sync
// right after processing to-device messages:
room.lastEvent = decrypted;
}
}
// and finally broadcast the new session

View File

@ -233,12 +233,8 @@ class Client extends MatrixApi {
EventTypes.CallAnswer,
EventTypes.CallReject,
EventTypes.CallHangup,
/// hack because having them both in important events and roomPreivew
/// makes the statekey '' which means you can only have one event of that
/// type
// EventTypes.GroupCallPrefix,
// EventTypes.GroupCallMemberPrefix,
EventTypes.GroupCallPrefix,
EventTypes.GroupCallMemberPrefix,
]);
// register all the default commands
@ -2131,7 +2127,12 @@ class Client extends MatrixApi {
final id = entry.key;
final syncRoomUpdate = entry.value;
await database?.storeRoomUpdate(id, syncRoomUpdate, this);
// Is the timeline limited? Then all previous messages should be
// removed from the database!
if (syncRoomUpdate is JoinedRoomUpdate &&
syncRoomUpdate.timeline?.limited == true) {
await database?.deleteTimelineForRoom(id);
}
final room = await _updateRoomsByRoomUpdate(id, syncRoomUpdate);
final timelineUpdateType = direction != null
@ -2202,6 +2203,7 @@ class Client extends MatrixApi {
await _handleRoomEvents(room, state, EventUpdateType.inviteState);
}
}
await database?.storeRoomUpdate(id, syncRoomUpdate, room.lastEvent, this);
}
}
@ -2478,50 +2480,55 @@ class Client extends MatrixApi {
if (eventUpdate.type == EventUpdateType.history) return;
switch (eventUpdate.type) {
case EventUpdateType.timeline:
case EventUpdateType.state:
case EventUpdateType.inviteState:
final stateEvent = Event.fromJson(eventUpdate.content, room);
if (stateEvent.type == EventTypes.Redaction) {
final String? redacts = eventUpdate.content.tryGet<String>('redacts');
if (redacts != null) {
room.states.forEach(
(String key, Map<String, Event> states) => states.forEach(
(String key, Event state) {
if (state.eventId == redacts) {
state.setRedactionEvent(stateEvent);
}
},
),
);
}
} else {
// We want to set state the in-memory cache for the room with the new event.
// To do this, we have to respect to not save edits, unless they edit the
// current last event.
// Additionally, we only store the event in-memory if the room has either been
// post-loaded or the event is animportant state event.
final noMessageOrNoEdit = stateEvent.type != EventTypes.Message ||
stateEvent.relationshipType != RelationshipTypes.edit;
final editingLastEvent =
stateEvent.relationshipEventId == room.lastEvent?.eventId;
final consecutiveEdit =
room.lastEvent?.relationshipType == RelationshipTypes.edit &&
stateEvent.relationshipEventId ==
room.lastEvent?.relationshipEventId;
final importantOrRoomLoaded =
eventUpdate.type == EventUpdateType.inviteState ||
!room.partial ||
// make sure we do overwrite events we have already loaded.
room.states[stateEvent.type]
?.containsKey(stateEvent.stateKey ?? '') ==
true ||
importantStateEvents.contains(stateEvent.type);
if ((noMessageOrNoEdit || editingLastEvent || consecutiveEdit) &&
importantOrRoomLoaded) {
room.setState(stateEvent);
}
room.setState(Event.fromJson(eventUpdate.content, room));
break;
case EventUpdateType.state:
case EventUpdateType.timeline:
final event = Event.fromJson(eventUpdate.content, room);
// Update the room state:
if (!room.partial ||
// make sure we do overwrite events we have already loaded.
room.states[event.type]?.containsKey(event.stateKey ?? '') ==
true ||
importantStateEvents.contains(event.type)) {
room.setState(event);
}
if (eventUpdate.type != EventUpdateType.timeline) break;
// If last event is null or not a valid room preview event anyway,
// just use this:
if (room.lastEvent == null ||
!roomPreviewLastEvents.contains(room.lastEvent?.type)) {
room.lastEvent = event;
break;
}
// Is this event redacting the last event?
if (event.type == EventTypes.Redaction &&
event.redacts == room.lastEvent?.eventId) {
room.lastEvent?.setRedactionEvent(event);
break;
}
// Is this event an edit of the last event? Otherwise ignore it.
if (event.relationshipType == RelationshipTypes.edit) {
if (event.relationshipEventId == room.lastEvent?.eventId ||
(room.lastEvent?.relationshipType == RelationshipTypes.edit &&
event.relationshipEventId ==
room.lastEvent?.relationshipEventId)) {
room.lastEvent = event;
}
break;
}
// Is this event of an important type for the last event?
if (!roomPreviewLastEvents.contains(event.type)) break;
// Event is a valid new lastEvent:
room.lastEvent = event;
break;
case EventUpdateType.accountData:
room.roomAccountData[eventUpdate.content['type']] =

View File

@ -65,7 +65,13 @@ abstract class DatabaseApi {
/// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeRoomUpdate(
String roomId, SyncRoomUpdate roomUpdate, Client client);
String roomId,
SyncRoomUpdate roomUpdate,
Event? lastEvent,
Client client,
);
Future<void> deleteTimelineForRoom(String roomId);
/// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction].

View File

@ -1130,12 +1130,7 @@ class HiveCollectionsDatabase extends DatabaseApi {
? ''
: eventUpdate.content['state_key'];
// Store a common state event
if ({
EventUpdateType.timeline,
EventUpdateType.state,
EventUpdateType.inviteState
}.contains(eventUpdate.type) &&
stateKey != null) {
if (stateKey != null) {
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
await _roomMembersBox.put(
TupleKey(
@ -1149,45 +1144,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
eventUpdate.content['type'],
).toString();
final stateMap = copyMap(await _roomStateBox.get(key) ?? {});
// store state events and new messages, that either are not an edit or an edit of the lastest message
// An edit is an event, that has an edit relation to the latest event. In some cases for the second edit, we need to compare if both have an edit relation to the same event instead.
if (eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to') ==
null) {
stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap);
} else {
final editedEventRelationshipEventId = eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to')
?.tryGet<String>('event_id');
final tmpRoom = client.getRoomById(eventUpdate.roomID) ??
Room(id: eventUpdate.roomID, client: client);
if (eventUpdate.content['type'] !=
EventTypes
.Message || // send anything other than a message
eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to')
?.tryGet<String>('rel_type') !=
RelationshipTypes
.edit || // replies are always latest anyway
editedEventRelationshipEventId ==
tmpRoom.lastEvent
?.eventId || // edit of latest (original event) event
(tmpRoom.lastEvent?.relationshipType ==
RelationshipTypes.edit &&
editedEventRelationshipEventId ==
tmpRoom.lastEvent
?.relationshipEventId) // edit of latest (edited event) event
) {
stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap);
}
}
stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap);
}
}
@ -1257,7 +1216,11 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> storeRoomUpdate(
String roomId, SyncRoomUpdate roomUpdate, Client client) async {
String roomId,
SyncRoomUpdate roomUpdate,
Event? lastEvent,
Client client,
) async {
// Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId);
@ -1287,11 +1250,13 @@ class HiveCollectionsDatabase extends DatabaseApi {
0,
prev_batch: roomUpdate.timeline?.prevBatch,
summary: roomUpdate.summary,
lastEvent: lastEvent,
).toJson()
: Room(
client: client,
id: roomId,
membership: membership,
lastEvent: lastEvent,
).toJson());
} else if (roomUpdate is JoinedRoomUpdate) {
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
@ -1311,17 +1276,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {})),
lastEvent: lastEvent,
).toJson());
}
// Is the timeline limited? Then all previous messages should be
// removed from the database!
if (roomUpdate is JoinedRoomUpdate &&
roomUpdate.timeline?.limited == true) {
await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
}
}
@override
Future<void> deleteTimelineForRoom(String roomId) =>
_timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
@override
Future<void> storeSSSSCache(
String type, String keyId, String ciphertext, String content) async {

View File

@ -1062,12 +1062,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
? ''
: eventUpdate.content['state_key'];
// Store a common state event
if ({
EventUpdateType.timeline,
EventUpdateType.state,
EventUpdateType.inviteState
}.contains(eventUpdate.type) &&
stateKey != null) {
if (stateKey != null) {
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
await _roomMembersBox.put(
MultiKey(
@ -1082,45 +1077,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
).toString();
final Map stateMap = await _roomStateBox.get(key) ?? {};
// store state events and new messages, that either are not an edit or an edit of the lastest message
// An edit is an event, that has an edit relation to the latest event. In some cases for the second edit, we need to compare if both have an edit relation to the same event instead.
if (eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to') ==
null) {
stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap);
} else {
final editedEventRelationshipEventId = eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to')
?.tryGet<String>('event_id');
final tmpRoom = client.getRoomById(eventUpdate.roomID) ??
Room(id: eventUpdate.roomID, client: client);
if (eventUpdate.content['type'] !=
EventTypes
.Message || // send anything other than a message
eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to')
?.tryGet<String>('rel_type') !=
RelationshipTypes
.edit || // replies are always latest anyway
editedEventRelationshipEventId ==
tmpRoom.lastEvent
?.eventId || // edit of latest (original event) event
(tmpRoom.lastEvent?.relationshipType ==
RelationshipTypes.edit &&
editedEventRelationshipEventId ==
tmpRoom.lastEvent
?.relationshipEventId) // edit of latest (edited event) event
) {
stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap);
}
}
stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap);
}
}
@ -1189,8 +1147,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
}
@override
Future<void> storeRoomUpdate(
String roomId, SyncRoomUpdate roomUpdate, Client client) async {
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
Event? lastEvent, Client client) async {
// Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId);
@ -1219,11 +1177,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
0,
prev_batch: roomUpdate.timeline?.prevBatch,
summary: roomUpdate.summary,
lastEvent: lastEvent,
).toJson()
: Room(
client: client,
id: roomId,
membership: membership,
lastEvent: lastEvent,
).toJson());
} else if (roomUpdate is JoinedRoomUpdate) {
final currentRawRoom = await _roomsBox.get(roomId.toHiveKey);
@ -1246,15 +1206,12 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
..addAll(roomUpdate.summary?.toJson() ?? {})),
).toJson());
}
// Is the timeline limited? Then all previous messages should be
// removed from the database!
if (roomUpdate is JoinedRoomUpdate &&
roomUpdate.timeline?.limited == true) {
await _timelineFragmentsBox.delete(MultiKey(roomId, '').toString());
}
}
@override
Future<void> deleteTimelineForRoom(String roomId) =>
_timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
@override
Future<void> storeSSSSCache(
String type, String keyId, String ciphertext, String content) async {

View File

@ -1089,12 +1089,7 @@ class MatrixSdkDatabase extends DatabaseApi {
? ''
: eventUpdate.content['state_key'];
// Store a common state event
if ({
EventUpdateType.timeline,
EventUpdateType.state,
EventUpdateType.inviteState
}.contains(eventUpdate.type) &&
stateKey != null) {
if (stateKey != null) {
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
await _roomMembersBox.put(
TupleKey(
@ -1112,45 +1107,9 @@ class MatrixSdkDatabase extends DatabaseApi {
type,
).toString();
final stateMap = copyMap(await roomStateBox.get(key) ?? {});
// store state events and new messages, that either are not an edit or an edit of the lastest message
// An edit is an event, that has an edit relation to the latest event. In some cases for the second edit, we need to compare if both have an edit relation to the same event instead.
if (eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to') ==
null) {
stateMap[stateKey] = eventUpdate.content;
await roomStateBox.put(key, stateMap);
} else {
final editedEventRelationshipEventId = eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to')
?.tryGet<String>('event_id');
final tmpRoom = client.getRoomById(eventUpdate.roomID) ??
Room(id: eventUpdate.roomID, client: client);
if (eventUpdate.content['type'] !=
EventTypes
.Message || // send anything other than a message
eventUpdate.content
.tryGetMap<String, dynamic>('content')
?.tryGetMap<String, dynamic>('m.relates_to')
?.tryGet<String>('rel_type') !=
RelationshipTypes
.edit || // replies are always latest anyway
editedEventRelationshipEventId ==
tmpRoom.lastEvent
?.eventId || // edit of latest (original event) event
(tmpRoom.lastEvent?.relationshipType ==
RelationshipTypes.edit &&
editedEventRelationshipEventId ==
tmpRoom.lastEvent
?.relationshipEventId) // edit of latest (edited event) event
) {
stateMap[stateKey] = eventUpdate.content;
await roomStateBox.put(key, stateMap);
}
}
stateMap[stateKey] = eventUpdate.content;
await roomStateBox.put(key, stateMap);
}
}
@ -1226,8 +1185,8 @@ class MatrixSdkDatabase extends DatabaseApi {
}
@override
Future<void> storeRoomUpdate(
String roomId, SyncRoomUpdate roomUpdate, Client client) async {
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
Event? lastEvent, Client client) async {
// Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId);
@ -1257,11 +1216,13 @@ class MatrixSdkDatabase extends DatabaseApi {
0,
prev_batch: roomUpdate.timeline?.prevBatch,
summary: roomUpdate.summary,
lastEvent: lastEvent,
).toJson()
: Room(
client: client,
id: roomId,
membership: membership,
lastEvent: lastEvent,
).toJson());
} else if (roomUpdate is JoinedRoomUpdate) {
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
@ -1281,17 +1242,15 @@ class MatrixSdkDatabase extends DatabaseApi {
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {})),
lastEvent: lastEvent,
).toJson());
}
// Is the timeline limited? Then all previous messages should be
// removed from the database!
if (roomUpdate is JoinedRoomUpdate &&
roomUpdate.timeline?.limited == true) {
await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
}
}
@override
Future<void> deleteTimelineForRoom(String roomId) =>
_timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
@override
Future<void> storeSSSSCache(
String type, String keyId, String ciphertext, String content) async {

View File

@ -111,21 +111,27 @@ class Room {
'notification_count': notificationCount,
'prev_batch': prev_batch,
'summary': summary.toJson(),
'last_event': lastEvent?.toJson(),
};
factory Room.fromJson(Map<String, dynamic> json, Client client) => Room(
client: client,
id: json['id'],
membership: Membership.values.singleWhere(
(m) => m.toString() == 'Membership.${json['membership']}',
orElse: () => Membership.join,
),
notificationCount: json['notification_count'],
highlightCount: json['highlight_count'],
prev_batch: json['prev_batch'],
summary:
RoomSummary.fromJson(Map<String, dynamic>.from(json['summary'])),
);
factory Room.fromJson(Map<String, dynamic> json, Client client) {
final room = Room(
client: client,
id: json['id'],
membership: Membership.values.singleWhere(
(m) => m.toString() == 'Membership.${json['membership']}',
orElse: () => Membership.join,
),
notificationCount: json['notification_count'],
highlightCount: json['highlight_count'],
prev_batch: json['prev_batch'],
summary: RoomSummary.fromJson(Map<String, dynamic>.from(json['summary'])),
);
if (json['last_event'] != null) {
room._lastEvent = Event.fromJson(json['last_event'], room);
}
return room;
}
/// Flag if the room is partial, meaning not all state events have been loaded yet
bool partial = true;
@ -157,50 +163,25 @@ class Room {
/// Adds the [state] to this room and overwrites a state with the same
/// typeKey/stateKey key pair if there is one.
void setState(Event state) {
// Decrypt if necessary
if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
try {
state = client.encryption?.decryptRoomEventSync(id, state) ?? state;
} catch (e, s) {
Logs().e('[LibOlm] Could not decrypt room state', e, s);
}
}
// We ignore room verification events for lastEvents
if (state.type == EventTypes.Message &&
state.messageType.startsWith('m.room.verification.')) {
return;
}
final isMessageEvent = {
EventTypes.Message,
EventTypes.Encrypted,
EventTypes.Sticker
}.contains(state.type);
// We ignore events editing events older than the current-latest here so
// i.e. newly sent edits for older events don't show up in room preview
final lastEvent = this.lastEvent;
if (isMessageEvent &&
state.relationshipEventId != null &&
state.relationshipType == RelationshipTypes.edit &&
lastEvent != null &&
!state.matchesEventOrTransactionId(lastEvent.eventId) &&
lastEvent.eventId != state.relationshipEventId &&
!(lastEvent.relationshipType == RelationshipTypes.edit &&
lastEvent.relationshipEventId == state.relationshipEventId)) {
return;
}
// Ignore other non-state events
final stateKey = state.stateKey ??
(client.roomPreviewLastEvents.contains(state.type) ? '' : null);
final stateKey = state.stateKey;
final roomId = state.roomId;
if (stateKey == null || roomId == null) {
if (roomId == null || roomId != id) {
Logs().w('Tried to set state event for wrong room!');
return;
}
if (stateKey == null && !client.importantStateEvents.contains(state.type)) {
Logs().w(
'Tried to set a non state event with type "${state.type}" as state event for a room',
);
return;
}
(states[state.type] ??= {})[stateKey] = state;
// We still store events without a state key to load legacy lastEvent
// candidates from the database. This can be changed once we either
// implemented a database migration for legacy lastEvent candidates or
// we just waited some time (written at March 13th 2024).
(states[state.type] ??= {})[stateKey ?? ''] = state;
client.onRoomState.add(state);
}
@ -384,7 +365,17 @@ class Room {
/// Must be one of [all, mention]
String? notificationSettings;
Event? _lastEvent;
set lastEvent(Event? event) {
_lastEvent = event;
}
Event? get lastEvent {
if (_lastEvent != null) return _lastEvent;
// !Everything below is the deprecated way of fetching the last event!
// as lastEvent calculation is based on the state events we unfortunately cannot
// use sortOrder here: With many state events we just know which ones are the
// newest ones, without knowing in which order they actually happened. As such,
@ -447,7 +438,9 @@ class Room {
this.notificationSettings,
Map<String, BasicRoomEvent>? roomAccountData,
RoomSummary? summary,
Event? lastEvent,
}) : roomAccountData = roomAccountData ?? <String, BasicRoomEvent>{},
_lastEvent = lastEvent,
summary = summary ??
RoomSummary.fromJson({
'm.joined_member_count': 0,

View File

@ -537,7 +537,7 @@ void main() {
}
}
}));
expect(room.getState('m.room.message')!.content['body'], 'meow');
expect(room.lastEvent!.content['body'], 'meow');
// ignore edits
await matrix.handleSync(SyncUpdate.fromJson({
@ -571,7 +571,7 @@ void main() {
}
}
}));
expect(room.getState('m.room.message')!.content['body'], 'meow');
expect(room.lastEvent!.content['body'], 'meow');
// accept edits to the last event
await matrix.handleSync(SyncUpdate.fromJson({
@ -605,7 +605,7 @@ void main() {
}
}
}));
expect(room.getState('m.room.message')!.content['body'], '* floooof');
expect(room.lastEvent!.content['body'], '* floooof');
// accepts a consecutive edit
await matrix.handleSync(SyncUpdate.fromJson({
@ -639,7 +639,7 @@ void main() {
}
}
}));
expect(room.getState('m.room.message')!.content['body'], '* foxies');
expect(room.lastEvent!.content['body'], '* foxies');
});
test('getProfileFromUserId', () async {

View File

@ -101,7 +101,7 @@ void main() {
'membership': Membership.join,
});
final client = Client('testclient');
await database.storeRoomUpdate('!testroom', roomUpdate, client);
await database.storeRoomUpdate('!testroom', roomUpdate, null, client);
final rooms = await database.getRoomList(client);
expect(rooms.single.id, '!testroom');
});

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
@ -25,6 +26,26 @@ import 'package:matrix/matrix.dart';
import 'fake_client.dart';
import 'fake_matrix_api.dart';
Future<void> updateLastEvent(Event event) {
if (event.room.client.getRoomById(event.room.id) == null) {
event.room.client.rooms.add(event.room);
}
return event.room.client.handleSync(
SyncUpdate(
rooms: RoomsUpdate(
join: {
event.room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [event],
),
),
},
),
nextBatch: '',
),
);
}
void main() {
late Client matrix;
late Room room;
@ -193,18 +214,19 @@ void main() {
expect(room.pinnedEventIds, <String>[]);
room.setState(
Event(
senderId: '@test:example.com',
type: 'm.room.pinned_events',
room: room,
eventId: '123',
content: {
'pinned': ['1234']
},
originServerTs: DateTime.now(),
stateKey: ''),
senderId: '@test:example.com',
type: 'm.room.pinned_events',
room: room,
eventId: '123',
content: {
'pinned': ['1234']
},
originServerTs: DateTime.now(),
stateKey: '',
),
);
expect(room.pinnedEventIds.first, '1234');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.message',
@ -212,7 +234,6 @@ void main() {
eventId: '12345',
originServerTs: DateTime.now(),
content: {'msgtype': 'm.text', 'body': 'abc'},
stateKey: '',
),
);
expect(room.lastEvent?.eventId, '12345');
@ -220,8 +241,19 @@ void main() {
expect(room.timeCreated, room.lastEvent?.originServerTs);
});
test('lastEvent is set properly', () {
room.setState(
test('lastEvent is set properly', () async {
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.message',
room: room,
eventId: '0',
originServerTs: DateTime.now(),
content: {'msgtype': 'm.text', 'body': 'meow'},
),
);
expect(room.lastEvent?.body, 'meow');
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -229,13 +261,12 @@ void main() {
eventId: '1',
originServerTs: DateTime.now(),
content: {'msgtype': 'm.text', 'body': 'cd'},
stateKey: '',
),
);
expect(room.hasNewMessages, isTrue);
expect(room.isUnreadOrInvited, isTrue);
expect(room.hasNewMessages, true);
expect(room.isUnreadOrInvited, false);
expect(room.lastEvent?.body, 'cd');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -243,11 +274,10 @@ void main() {
eventId: '2',
originServerTs: DateTime.now(),
content: {'msgtype': 'm.text', 'body': 'cdc'},
stateKey: '',
),
);
expect(room.lastEvent?.body, 'cdc');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -260,11 +290,10 @@ void main() {
'msgtype': 'm.text',
'body': '* test ok',
},
stateKey: '',
),
);
expect(room.lastEvent?.body, 'cdc'); // because we edited the "cd" message
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -277,7 +306,10 @@ void main() {
'm.new_content': {'msgtype': 'm.text', 'body': 'edited cdc'},
'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'},
},
stateKey: '',
unsigned: {
messageSendingStatusKey: EventStatus.sending.intValue,
'transaction_id': 'messageID',
},
status: EventStatus.sending,
),
);
@ -286,13 +318,16 @@ void main() {
expect(room.lastEvent?.eventId, '4');
// Status update on edits working?
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
room: room,
eventId: '5',
unsigned: {'transaction_id': '4'},
unsigned: {
'transaction_id': '4',
messageSendingStatusKey: EventStatus.sent.intValue,
},
originServerTs: DateTime.now(),
content: {
'msgtype': 'm.text',
@ -308,7 +343,7 @@ void main() {
expect(room.lastEvent?.body, 'edited cdc');
expect(room.lastEvent?.status, EventStatus.sent);
// Are reactions coming through?
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: EventTypes.Reaction,
@ -322,15 +357,15 @@ void main() {
'key': ':-)',
}
},
stateKey: '',
),
);
expect(room.lastEvent?.eventId, '5');
expect(room.lastEvent?.body, 'edited cdc');
expect(room.lastEvent?.status, EventStatus.sent);
});
test('lastEvent when reply parent edited', () async {
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -338,12 +373,11 @@ void main() {
eventId: '5',
originServerTs: DateTime.now(),
content: {'msgtype': 'm.text', 'body': 'A'},
stateKey: '',
),
);
expect(room.lastEvent?.body, 'A');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -355,11 +389,10 @@ void main() {
'body': 'B',
'm.relates_to': {'rel_type': 'm.in_reply_to', 'event_id': '5'}
},
stateKey: '',
),
);
expect(room.lastEvent?.body, 'B');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -372,14 +405,13 @@ void main() {
'm.new_content': {'msgtype': 'm.text', 'body': 'edited A'},
'm.relates_to': {'rel_type': 'm.replace', 'event_id': '5'},
},
stateKey: '',
),
);
expect(room.lastEvent?.body, 'B');
});
test('lastEvent with deleted message', () async {
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -392,7 +424,7 @@ void main() {
);
expect(room.lastEvent?.body, 'AA');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -409,7 +441,7 @@ void main() {
);
expect(room.lastEvent?.body, 'B');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -428,7 +460,7 @@ void main() {
),
);
expect(room.lastEvent?.eventId, '10');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -440,7 +472,7 @@ void main() {
),
);
expect(room.lastEvent?.body, 'BB');
room.setState(
await updateLastEvent(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
@ -815,7 +847,7 @@ void main() {
test('getTimeline', () async {
final timeline = await room.getTimeline();
expect(timeline.events.length, 0);
expect(timeline.events.length, 14);
});
test('getUserByMXID', () async {
@ -1260,7 +1292,7 @@ void main() {
},
room,
));
expect(room.getState('m.room.message') != null, true);
expect(room.getState('m.room.message') == null, false);
});
test('Widgets', () {

View File

@ -132,7 +132,14 @@ void main() => group('Integration tests', () {
Logs().i('++++ (Alice) Enable encryption ++++');
expect(room.encrypted, false);
await room.enableEncryption();
await Future.delayed(Duration(seconds: 5));
var waitSeconds = 0;
while (!room.encrypted) {
await Future.delayed(Duration(seconds: 1));
waitSeconds++;
if (waitSeconds >= 60) {
throw Exception('Unable to enable encryption');
}
}
expect(room.encrypted, isTrue);
expect(
room.client.encryption!.keyManager