Merge pull request #1721 from famedly/krille/last-event-stored-in-room

refactor: Store lastEvent in room object instead of room state
This commit is contained in:
Krille-chan 2024-03-18 14:20:58 +01:00 committed by GitHub
commit 3fca8365b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 249 additions and 373 deletions

View File

@ -193,22 +193,9 @@ class KeyManager {
event.content['session_id'] == sessionId) { event.content['session_id'] == sessionId) {
final decrypted = encryption.decryptRoomEventSync(roomId, event); final decrypted = encryption.decryptRoomEventSync(roomId, event);
if (decrypted.type != EventTypes.Encrypted) { if (decrypted.type != EventTypes.Encrypted) {
// Remove this from the state to make sure it does not appear as last event // No need to persist it as the lastEvent is persisted in the sync
room.states.remove(EventTypes.Encrypted); // right after processing to-device messages:
// Set the decrypted event as last event by adding it to the state room.lastEvent = decrypted;
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,
);
}
} }
} }
// and finally broadcast the new session // and finally broadcast the new session

View File

@ -216,8 +216,6 @@ class Client extends MatrixApi {
importantStateEvents.addAll([ importantStateEvents.addAll([
EventTypes.RoomName, EventTypes.RoomName,
EventTypes.RoomAvatar, EventTypes.RoomAvatar,
EventTypes.Message,
EventTypes.Encrypted,
EventTypes.Encryption, EventTypes.Encryption,
EventTypes.RoomCanonicalAlias, EventTypes.RoomCanonicalAlias,
EventTypes.RoomTombstone, EventTypes.RoomTombstone,
@ -233,12 +231,8 @@ class Client extends MatrixApi {
EventTypes.CallAnswer, EventTypes.CallAnswer,
EventTypes.CallReject, EventTypes.CallReject,
EventTypes.CallHangup, EventTypes.CallHangup,
EventTypes.GroupCallPrefix,
/// hack because having them both in important events and roomPreivew EventTypes.GroupCallMemberPrefix,
/// makes the statekey '' which means you can only have one event of that
/// type
// EventTypes.GroupCallPrefix,
// EventTypes.GroupCallMemberPrefix,
]); ]);
// register all the default commands // register all the default commands
@ -2131,7 +2125,12 @@ class Client extends MatrixApi {
final id = entry.key; final id = entry.key;
final syncRoomUpdate = entry.value; 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 room = await _updateRoomsByRoomUpdate(id, syncRoomUpdate);
final timelineUpdateType = direction != null final timelineUpdateType = direction != null
@ -2202,6 +2201,7 @@ class Client extends MatrixApi {
await _handleRoomEvents(room, state, EventUpdateType.inviteState); await _handleRoomEvents(room, state, EventUpdateType.inviteState);
} }
} }
await database?.storeRoomUpdate(id, syncRoomUpdate, room.lastEvent, this);
} }
} }
@ -2478,50 +2478,56 @@ class Client extends MatrixApi {
if (eventUpdate.type == EventUpdateType.history) return; if (eventUpdate.type == EventUpdateType.history) return;
switch (eventUpdate.type) { switch (eventUpdate.type) {
case EventUpdateType.timeline:
case EventUpdateType.state:
case EventUpdateType.inviteState: case EventUpdateType.inviteState:
final stateEvent = Event.fromJson(eventUpdate.content, room); room.setState(Event.fromJson(eventUpdate.content, room));
if (stateEvent.type == EventTypes.Redaction) { break;
final String? redacts = eventUpdate.content.tryGet<String>('redacts'); case EventUpdateType.state:
if (redacts != null) { case EventUpdateType.timeline:
room.states.forEach( final event = Event.fromJson(eventUpdate.content, room);
(String key, Map<String, Event> states) => states.forEach(
(String key, Event state) { // Update the room state:
if (state.eventId == redacts) { if (!room.partial ||
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. // make sure we do overwrite events we have already loaded.
room.states[stateEvent.type] room.states[event.type]?.containsKey(event.stateKey ?? '') ==
?.containsKey(stateEvent.stateKey ?? '') ==
true || true ||
importantStateEvents.contains(stateEvent.type); importantStateEvents.contains(event.type)) {
if ((noMessageOrNoEdit || editingLastEvent || consecutiveEdit) && room.setState(event);
importantOrRoomLoaded) {
room.setState(stateEvent);
} }
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.content.tryGet<String>('redacts') ?? 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; break;
case EventUpdateType.accountData: case EventUpdateType.accountData:
room.roomAccountData[eventUpdate.content['type']] = 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 /// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction]. /// [transaction].
Future<void> storeRoomUpdate( 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 /// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction]. /// [transaction].

View File

@ -35,7 +35,7 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
/// This database does not support file caching! /// This database does not support file caching!
class HiveCollectionsDatabase extends DatabaseApi { class HiveCollectionsDatabase extends DatabaseApi {
static const int version = 6; static const int version = 7;
final String name; final String name;
final String? path; final String? path;
final HiveCipher? key; final HiveCipher? key;
@ -1125,17 +1125,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
await removeEvent(transactionId, eventUpdate.roomID); await removeEvent(transactionId, eventUpdate.roomID);
} }
} }
final stateKey =
client.roomPreviewLastEvents.contains(eventUpdate.content['type']) final stateKey = eventUpdate.content['state_key'];
? ''
: eventUpdate.content['state_key'];
// Store a common state event // Store a common state event
if ({ if (stateKey != null) {
EventUpdateType.timeline,
EventUpdateType.state,
EventUpdateType.inviteState
}.contains(eventUpdate.type) &&
stateKey != null) {
if (eventUpdate.content['type'] == EventTypes.RoomMember) { if (eventUpdate.content['type'] == EventTypes.RoomMember) {
await _roomMembersBox.put( await _roomMembersBox.put(
TupleKey( TupleKey(
@ -1149,45 +1142,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
eventUpdate.content['type'], eventUpdate.content['type'],
).toString(); ).toString();
final stateMap = copyMap(await _roomStateBox.get(key) ?? {}); 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; stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap); 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);
}
}
} }
} }
@ -1257,7 +1214,11 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override @override
Future<void> storeRoomUpdate( 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 // Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) { if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId); await forgetRoom(roomId);
@ -1287,11 +1248,13 @@ class HiveCollectionsDatabase extends DatabaseApi {
0, 0,
prev_batch: roomUpdate.timeline?.prevBatch, prev_batch: roomUpdate.timeline?.prevBatch,
summary: roomUpdate.summary, summary: roomUpdate.summary,
lastEvent: lastEvent,
).toJson() ).toJson()
: Room( : Room(
client: client, client: client,
id: roomId, id: roomId,
membership: membership, membership: membership,
lastEvent: lastEvent,
).toJson()); ).toJson());
} else if (roomUpdate is JoinedRoomUpdate) { } else if (roomUpdate is JoinedRoomUpdate) {
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client); final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
@ -1311,16 +1274,14 @@ class HiveCollectionsDatabase extends DatabaseApi {
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch, roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(currentRoom.summary.toJson() summary: RoomSummary.fromJson(currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {})), ..addAll(roomUpdate.summary?.toJson() ?? {})),
lastEvent: lastEvent,
).toJson()); ).toJson());
} }
}
// Is the timeline limited? Then all previous messages should be @override
// removed from the database! Future<void> deleteTimelineForRoom(String roomId) =>
if (roomUpdate is JoinedRoomUpdate && _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
roomUpdate.timeline?.limited == true) {
await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
}
}
@override @override
Future<void> storeSSSSCache( Future<void> storeSSSSCache(

View File

@ -41,7 +41,7 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
@Deprecated( @Deprecated(
'Use [HiveCollectionsDatabase] instead. Don\'t forget to properly migrate!') 'Use [HiveCollectionsDatabase] instead. Don\'t forget to properly migrate!')
class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
static const int version = 5; static const int version = 6;
final String name; final String name;
late Box _clientBox; late Box _clientBox;
late Box _accountDataBox; late Box _accountDataBox;
@ -1057,17 +1057,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
} }
} }
final stateKey = final stateKey = eventUpdate.content['state_key'];
client.roomPreviewLastEvents.contains(eventUpdate.content['type'])
? ''
: eventUpdate.content['state_key'];
// Store a common state event // Store a common state event
if ({ if (stateKey != null) {
EventUpdateType.timeline,
EventUpdateType.state,
EventUpdateType.inviteState
}.contains(eventUpdate.type) &&
stateKey != null) {
if (eventUpdate.content['type'] == EventTypes.RoomMember) { if (eventUpdate.content['type'] == EventTypes.RoomMember) {
await _roomMembersBox.put( await _roomMembersBox.put(
MultiKey( MultiKey(
@ -1082,45 +1074,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
).toString(); ).toString();
final Map stateMap = await _roomStateBox.get(key) ?? {}; 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; stateMap[stateKey] = eventUpdate.content;
await _roomStateBox.put(key, stateMap); 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);
}
}
} }
} }
@ -1189,8 +1144,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
} }
@override @override
Future<void> storeRoomUpdate( Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
String roomId, SyncRoomUpdate roomUpdate, Client client) async { Event? lastEvent, Client client) async {
// Leave room if membership is leave // Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) { if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId); await forgetRoom(roomId);
@ -1219,11 +1174,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
0, 0,
prev_batch: roomUpdate.timeline?.prevBatch, prev_batch: roomUpdate.timeline?.prevBatch,
summary: roomUpdate.summary, summary: roomUpdate.summary,
lastEvent: lastEvent,
).toJson() ).toJson()
: Room( : Room(
client: client, client: client,
id: roomId, id: roomId,
membership: membership, membership: membership,
lastEvent: lastEvent,
).toJson()); ).toJson());
} else if (roomUpdate is JoinedRoomUpdate) { } else if (roomUpdate is JoinedRoomUpdate) {
final currentRawRoom = await _roomsBox.get(roomId.toHiveKey); final currentRawRoom = await _roomsBox.get(roomId.toHiveKey);
@ -1246,14 +1203,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
..addAll(roomUpdate.summary?.toJson() ?? {})), ..addAll(roomUpdate.summary?.toJson() ?? {})),
).toJson()); ).toJson());
} }
}
// Is the timeline limited? Then all previous messages should be @override
// removed from the database! Future<void> deleteTimelineForRoom(String roomId) =>
if (roomUpdate is JoinedRoomUpdate && _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
roomUpdate.timeline?.limited == true) {
await _timelineFragmentsBox.delete(MultiKey(roomId, '').toString());
}
}
@override @override
Future<void> storeSSSSCache( Future<void> storeSSSSCache(

View File

@ -37,7 +37,7 @@ import 'package:matrix/src/database/indexeddb_box.dart'
if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart'; if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';
class MatrixSdkDatabase extends DatabaseApi { class MatrixSdkDatabase extends DatabaseApi {
static const int version = 6; static const int version = 7;
final String name; final String name;
late BoxCollection _collection; late BoxCollection _collection;
late Box<String> _clientBox; late Box<String> _clientBox;
@ -1084,17 +1084,9 @@ class MatrixSdkDatabase extends DatabaseApi {
} }
} }
final stateKey = final stateKey = eventUpdate.content['state_key'];
client.roomPreviewLastEvents.contains(eventUpdate.content['type'])
? ''
: eventUpdate.content['state_key'];
// Store a common state event // Store a common state event
if ({ if (stateKey != null) {
EventUpdateType.timeline,
EventUpdateType.state,
EventUpdateType.inviteState
}.contains(eventUpdate.type) &&
stateKey != null) {
if (eventUpdate.content['type'] == EventTypes.RoomMember) { if (eventUpdate.content['type'] == EventTypes.RoomMember) {
await _roomMembersBox.put( await _roomMembersBox.put(
TupleKey( TupleKey(
@ -1112,45 +1104,9 @@ class MatrixSdkDatabase extends DatabaseApi {
type, type,
).toString(); ).toString();
final stateMap = copyMap(await roomStateBox.get(key) ?? {}); 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; stateMap[stateKey] = eventUpdate.content;
await roomStateBox.put(key, stateMap); 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);
}
}
} }
} }
@ -1226,8 +1182,8 @@ class MatrixSdkDatabase extends DatabaseApi {
} }
@override @override
Future<void> storeRoomUpdate( Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
String roomId, SyncRoomUpdate roomUpdate, Client client) async { Event? lastEvent, Client client) async {
// Leave room if membership is leave // Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) { if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId); await forgetRoom(roomId);
@ -1257,11 +1213,13 @@ class MatrixSdkDatabase extends DatabaseApi {
0, 0,
prev_batch: roomUpdate.timeline?.prevBatch, prev_batch: roomUpdate.timeline?.prevBatch,
summary: roomUpdate.summary, summary: roomUpdate.summary,
lastEvent: lastEvent,
).toJson() ).toJson()
: Room( : Room(
client: client, client: client,
id: roomId, id: roomId,
membership: membership, membership: membership,
lastEvent: lastEvent,
).toJson()); ).toJson());
} else if (roomUpdate is JoinedRoomUpdate) { } else if (roomUpdate is JoinedRoomUpdate) {
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client); final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
@ -1281,16 +1239,14 @@ class MatrixSdkDatabase extends DatabaseApi {
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch, roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(currentRoom.summary.toJson() summary: RoomSummary.fromJson(currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {})), ..addAll(roomUpdate.summary?.toJson() ?? {})),
lastEvent: lastEvent,
).toJson()); ).toJson());
} }
}
// Is the timeline limited? Then all previous messages should be @override
// removed from the database! Future<void> deleteTimelineForRoom(String roomId) =>
if (roomUpdate is JoinedRoomUpdate && _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
roomUpdate.timeline?.limited == true) {
await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
}
}
@override @override
Future<void> storeSSSSCache( Future<void> storeSSSSCache(

View File

@ -111,9 +111,11 @@ class Room {
'notification_count': notificationCount, 'notification_count': notificationCount,
'prev_batch': prev_batch, 'prev_batch': prev_batch,
'summary': summary.toJson(), 'summary': summary.toJson(),
'last_event': lastEvent?.toJson(),
}; };
factory Room.fromJson(Map<String, dynamic> json, Client client) => Room( factory Room.fromJson(Map<String, dynamic> json, Client client) {
final room = Room(
client: client, client: client,
id: json['id'], id: json['id'],
membership: Membership.values.singleWhere( membership: Membership.values.singleWhere(
@ -123,9 +125,13 @@ class Room {
notificationCount: json['notification_count'], notificationCount: json['notification_count'],
highlightCount: json['highlight_count'], highlightCount: json['highlight_count'],
prev_batch: json['prev_batch'], prev_batch: json['prev_batch'],
summary: summary: RoomSummary.fromJson(Map<String, dynamic>.from(json['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 /// Flag if the room is partial, meaning not all state events have been loaded yet
bool partial = true; bool partial = true;
@ -157,46 +163,17 @@ class Room {
/// Adds the [state] to this room and overwrites a state with the same /// Adds the [state] to this room and overwrites a state with the same
/// typeKey/stateKey key pair if there is one. /// typeKey/stateKey key pair if there is one.
void setState(Event state) { 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 // Ignore other non-state events
final stateKey = state.stateKey ?? final stateKey = state.stateKey;
(client.roomPreviewLastEvents.contains(state.type) ? '' : null);
final roomId = state.roomId; 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) {
Logs().w(
'Tried to set a non state event with type "${state.type}" as state event for a room',
);
return; return;
} }
@ -384,32 +361,20 @@ class Room {
/// Must be one of [all, mention] /// Must be one of [all, mention]
String? notificationSettings; String? notificationSettings;
Event? get lastEvent { Event? _lastEvent;
// 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,
// using the origin_server_ts is the best guess for this algorithm. While not
// perfect, it is only used for the room preview in the room list and sorting
// said room list, so it should be good enough.
var lastTime = DateTime.fromMillisecondsSinceEpoch(0);
final lastEvents =
client.roomPreviewLastEvents.map(getState).whereType<Event>();
var lastEvent = lastEvents.isEmpty set lastEvent(Event? event) {
? null _lastEvent = event;
: lastEvents.reduce((a, b) {
if (a.originServerTs == b.originServerTs) {
// if two events have the same sort order we want to give encrypted events a lower priority
// This is so that if the same event exists in the state both encrypted *and* unencrypted,
// the unencrypted one is picked
return a.type == EventTypes.Encrypted ? b : a;
} }
return a.originServerTs.millisecondsSinceEpoch >
b.originServerTs.millisecondsSinceEpoch Event? get lastEvent {
? a if (_lastEvent != null) return _lastEvent;
: b;
}); // Just pick the newest state event as an indicator for when the last
if (lastEvent == null) { // activity was in this room. This is better than nothing:
var lastTime = DateTime.fromMillisecondsSinceEpoch(0);
Event? lastEvent;
states.forEach((final String key, final entry) { states.forEach((final String key, final entry) {
final state = entry['']; final state = entry[''];
if (state == null) return; if (state == null) return;
@ -419,7 +384,7 @@ class Room {
lastEvent = state; lastEvent = state;
} }
}); });
}
return lastEvent; return lastEvent;
} }
@ -447,7 +412,9 @@ class Room {
this.notificationSettings, this.notificationSettings,
Map<String, BasicRoomEvent>? roomAccountData, Map<String, BasicRoomEvent>? roomAccountData,
RoomSummary? summary, RoomSummary? summary,
Event? lastEvent,
}) : roomAccountData = roomAccountData ?? <String, BasicRoomEvent>{}, }) : roomAccountData = roomAccountData ?? <String, BasicRoomEvent>{},
_lastEvent = lastEvent,
summary = summary ?? summary = summary ??
RoomSummary.fromJson({ RoomSummary.fromJson({
'm.joined_member_count': 0, '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 // ignore edits
await matrix.handleSync(SyncUpdate.fromJson({ 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 // accept edits to the last event
await matrix.handleSync(SyncUpdate.fromJson({ 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 // accepts a consecutive edit
await matrix.handleSync(SyncUpdate.fromJson({ 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 { test('getProfileFromUserId', () async {

View File

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

View File

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

View File

@ -132,7 +132,14 @@ void main() => group('Integration tests', () {
Logs().i('++++ (Alice) Enable encryption ++++'); Logs().i('++++ (Alice) Enable encryption ++++');
expect(room.encrypted, false); expect(room.encrypted, false);
await room.enableEncryption(); 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.encrypted, isTrue);
expect( expect(
room.client.encryption!.keyManager room.client.encryption!.keyManager