From 67ddd978b491391528027846b4e69da9f58cdfc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Fri, 2 Jan 2026 15:24:38 +0100 Subject: [PATCH 1/2] refactor: Remove archive cache from Client --- lib/src/client.dart | 150 +++++++++-------------------------- lib/src/room.dart | 17 ---- test/client_test.dart | 27 ------- test/room_archived_test.dart | 137 +++----------------------------- 4 files changed, 50 insertions(+), 281 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 9d8913903..e70bfd99e 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -411,12 +411,6 @@ class Client extends MatrixApi { List get rooms => _rooms; List _rooms = []; - /// Get a list of the archived rooms - /// - /// Attention! Archived rooms are only returned if [loadArchive()] was called - /// beforehand! The state refers to the last retrieval via [loadArchive()]! - List get archivedRooms => _archivedRooms; - bool enableDehydratedDevices = false; final String dehydratedDeviceDisplayName; @@ -486,7 +480,7 @@ class Client extends MatrixApi { /// found. If you have loaded the [loadArchive()] before, it can also return /// archived rooms. Room? getRoomById(String id) { - for (final room in [...rooms, ..._archivedRooms.map((e) => e.room)]) { + for (final room in rooms) { if (room.id == id) return room; } @@ -1226,25 +1220,6 @@ class Client extends MatrixApi { ); } - final List _archivedRooms = []; - - /// Return an archive room containing the room and the timeline for a specific archived room. - ArchivedRoom? getArchiveRoomFromCache(String roomId) { - for (var i = 0; i < _archivedRooms.length; i++) { - final archive = _archivedRooms[i]; - if (archive.room.id == roomId) return archive; - } - return null; - } - - /// Remove all the archives stored in cache. - void clearArchivesFromCache() { - _archivedRooms.clear(); - } - - @Deprecated('Use [loadArchive()] instead.') - Future> get archive => loadArchive(); - /// Fetch all the archived rooms from the server and return the list of the /// room. If you want to have the Timelines bundled with it, use /// loadArchiveWithTimeline instead. @@ -1264,8 +1239,6 @@ class Client extends MatrixApi { /// Fetch the archived rooms from the server and return them as a list of /// [ArchivedRoom] objects containing the [Room] and the associated [Timeline]. Future> loadArchiveWithTimeline() async { - _archivedRooms.clear(); - final filter = jsonEncode( Filter( room: RoomFilter( @@ -1285,9 +1258,44 @@ class Client extends MatrixApi { _archiveCacheBusterTimeout = (_archiveCacheBusterTimeout + 1) % 30; final leave = syncResp.rooms?.leave; + final archivedRooms = []; if (leave != null) { for (final entry in leave.entries) { - await _storeArchivedRoom(entry.key, entry.value); + final room = Room( + id: entry.key, + prev_batch: entry.value.timeline?.prevBatch, + membership: Membership.leave, + client: this, + roomAccountData: + entry.value.accountData?.asMap().map( + (k, v) => MapEntry(v.type, v), + ) ?? + {}, + ); + entry.value.state?.forEach(room.setState); + final timeline = Timeline( + room: room, + chunk: TimelineChunk( + events: + entry.value.timeline?.events?.reversed + .toList() // we display the event in the other sence + .map((e) => Event.fromMatrixEvent(e, room)) + .toList() ?? + [], + ), + ); + for (var i = 0; i < timeline.events.length; i++) { + // Try to decrypt encrypted events but don't update the database. + if (room.encrypted && room.client.encryptionEnabled) { + if (timeline.events[i].type == EventTypes.Encrypted) { + await room.client.encryption! + .decryptRoomEvent(timeline.events[i]) + .then((decrypted) => timeline.events[i] = decrypted); + } + } + } + + archivedRooms.add(ArchivedRoom(room: room, timeline: timeline)); } } @@ -1295,84 +1303,13 @@ class Client extends MatrixApi { // best indicator we have to sort them. For archived rooms where we don't // have any, we move them to the bottom. final beginningOfTime = DateTime.fromMillisecondsSinceEpoch(0); - _archivedRooms.sort( + archivedRooms.sort( (b, a) => (a.room.lastEvent?.originServerTs ?? beginningOfTime).compareTo( b.room.lastEvent?.originServerTs ?? beginningOfTime, ), ); - return _archivedRooms; - } - - /// [_storeArchivedRoom] - /// @leftRoom we can pass a room which was left so that we don't loose states - Future _storeArchivedRoom( - String id, - LeftRoomUpdate update, { - Room? leftRoom, - }) async { - final roomUpdate = update; - final archivedRoom = - leftRoom ?? - Room( - id: id, - membership: Membership.leave, - client: this, - roomAccountData: - roomUpdate.accountData?.asMap().map( - (k, v) => MapEntry(v.type, v), - ) ?? - {}, - ); - // Set membership of room to leave, in the case we got a left room passed, otherwise - // the left room would have still membership join, which would be wrong for the setState later - archivedRoom.membership = Membership.leave; - final timeline = Timeline( - room: archivedRoom, - chunk: TimelineChunk( - events: - roomUpdate.timeline?.events?.reversed - .toList() // we display the event in the other seence - .map((e) => Event.fromMatrixEvent(e, archivedRoom)) - .toList() ?? - [], - ), - ); - - archivedRoom.prev_batch = update.timeline?.prevBatch; - - final stateEvents = roomUpdate.state; - if (stateEvents != null) { - await _handleRoomEvents( - archivedRoom, - stateEvents, - EventUpdateType.state, - store: false, - ); - } - - final timelineEvents = roomUpdate.timeline?.events; - if (timelineEvents != null) { - await _handleRoomEvents( - archivedRoom, - timelineEvents.toList(), - EventUpdateType.timeline, - store: false, - ); - } - - for (var i = 0; i < timeline.events.length; i++) { - // Try to decrypt encrypted events but don't update the database. - if (archivedRoom.encrypted && archivedRoom.client.encryptionEnabled) { - if (timeline.events[i].type == EventTypes.Encrypted) { - await archivedRoom.client.encryption! - .decryptRoomEvent(timeline.events[i]) - .then((decrypted) => timeline.events[i] = decrypted); - } - } - } - - _archivedRooms.add(ArchivedRoom(room: archivedRoom, timeline: timeline)); + return archivedRooms; } @override @@ -3117,10 +3054,6 @@ class Client extends MatrixApi { // Does the chat already exist in the list rooms? if (!found && membership != Membership.leave) { - // Check if the room is not in the rooms in the invited list - if (_archivedRooms.isNotEmpty) { - _archivedRooms.removeWhere((archive) => archive.room.id == roomId); - } final position = membership == Membership.invite ? 0 : rooms.length; // Add the new chat to the list rooms.insert(position, room); @@ -3128,11 +3061,6 @@ class Client extends MatrixApi { // If the membership is "leave" then remove the item and stop here else if (found && membership == Membership.leave) { rooms.removeAt(roomIndex); - - // in order to keep the archive in sync, add left room to archive - if (chatUpdate is LeftRoomUpdate) { - await _storeArchivedRoom(room.id, chatUpdate, leftRoom: room); - } } // Update notification, highlight count and/or additional information else if (found && diff --git a/lib/src/room.dart b/lib/src/room.dart index 9631c075c..199d9212e 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -1495,12 +1495,6 @@ class Room { Future forget() async { await client.database.forgetRoom(id); await client.forgetRoom(id); - // Update archived rooms, otherwise an archived room may still be in the - // list after a forget room call - final roomIndex = client.archivedRooms.indexWhere((r) => r.room.id == id); - if (roomIndex != -1) { - client.archivedRooms.removeAt(roomIndex); - } return; } @@ -1776,17 +1770,6 @@ class Room { await client.database.transaction(() async { events = await client.database.getEventList(this, limit: limit); }); - } else { - final archive = client.getArchiveRoomFromCache(id); - events = archive?.timeline.events.toList() ?? []; - for (var i = 0; i < events.length; i++) { - // Try to decrypt encrypted events but don't update the database. - if (encrypted && client.encryptionEnabled) { - if (events[i].type == EventTypes.Encrypted) { - events[i] = await client.encryption!.decryptRoomEvent(events[i]); - } - } - } } var chunk = TimelineChunk(events: events); diff --git a/test/client_test.dart b/test/client_test.dart index 7c791fded..0a209eb9c 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -1730,11 +1730,6 @@ void main() { reason: 'Count of invited+joined before loadArchive() rooms does not match', ); - expect( - client.archivedRooms.length, - 0, - reason: 'Count of archived rooms before loadArchive() does not match', - ); await client.loadArchive(); @@ -1743,28 +1738,6 @@ void main() { 3, reason: 'Count of invited+joined rooms does not match', ); - expect( - client.archivedRooms.length, - 2, - reason: 'Count of archived rooms does not match', - ); - - expect( - client.archivedRooms.firstWhereOrNull( - (r) => r.room.id == '!5345234234:example.com', - ) != - null, - true, - reason: '!5345234234:example.com not found as archived room', - ); - expect( - client.archivedRooms.firstWhereOrNull( - (r) => r.room.id == '!5345234235:example.com', - ) != - null, - true, - reason: '!5345234235:example.com not found as archived room', - ); await client.dispose(); }); diff --git a/test/room_archived_test.dart b/test/room_archived_test.dart index d66597c1e..4200c9995 100644 --- a/test/room_archived_test.dart +++ b/test/room_archived_test.dart @@ -28,13 +28,6 @@ void main() async { tearDown(() async => client.dispose().onError((e, s) {})); - test('archive room not loaded', () async { - final archiveRoom = client.getArchiveRoomFromCache( - '!5345234234:example.com', - ); - expect(archiveRoom, null); - }); - test('get archive', () async { final archive = await client.loadArchiveWithTimeline(); @@ -42,62 +35,37 @@ void main() async { expect(archive[0].room.id, '!5345234234:example.com'); expect(archive[0].room.membership, Membership.leave); expect(archive[0].room.name, 'The room name'); - expect( - archive[0].room.lastEvent?.body, - 'This is a second text example message', - ); expect(archive[0].room.roomAccountData.length, 1); expect(archive[1].room.id, '!5345234235:example.com'); expect(archive[1].room.membership, Membership.leave); expect(archive[1].room.name, 'The room name 2'); - - final archiveRoom = client.getArchiveRoomFromCache( - '!5345234234:example.com', - ); - expect(archiveRoom != null, true); - expect(archiveRoom!.timeline.events.length, 2); }); test('request history', () async { - await client.loadArchiveWithTimeline(); - final archiveRoom = client.getRoomById('!5345234234:example.com'); - expect(archiveRoom != null, true); + final archive = await client.loadArchiveWithTimeline(); - final timeline = await archiveRoom!.getTimeline(onInsert: insertList.add); + final timeline = archive.first.timeline; expect(timeline.events.length, 2); expect(timeline.events[0].eventId, '1532735824654:example.org'); expect(timeline.events[1].eventId, '1532735824650:example.org'); - expect( - archiveRoom.lastEvent?.body, - 'This is a second text example message', - ); await timeline.requestHistory(); - expect(timeline.events.length, 6); - expect(timeline.events[0].eventId, '1532735824654:example.org'); - expect(timeline.events[1].eventId, '1532735824650:example.org'); - expect(timeline.events[2].eventId, '1432735824656:example.org'); - expect(timeline.events[3].eventId, '1432735824655:example.org'); - expect(timeline.events[4].eventId, '1432735824654:example.org'); - expect(timeline.events[5].eventId, '1432735824653:example.org'); - - expect(insertList.length, 4); - - expect( - archiveRoom.lastEvent?.body, - 'This is a second text example message', - ); + expect(timeline.events.length, 5); + expect(timeline.events[0].eventId, '143274597443PhrSn:example.org'); + expect(timeline.events[1].eventId, '143274597446PhrSn:example.org'); + expect(timeline.events[2].eventId, '3143273582443PhrSn:example.org'); + expect(timeline.events[3].eventId, '2143273582443PhrSn:example.org'); + expect(timeline.events[4].eventId, '1143273582466PhrSn:example.org'); }); test('expect database to be empty', () async { - await client.loadArchiveWithTimeline(); - final archiveRoom = client.getRoomById('!5345234234:example.com'); - expect(archiveRoom != null, true); + final archive = await client.loadArchiveWithTimeline(); + final archiveRoom = archive.first; final eventsFromStore = await client.database.getEventList( - archiveRoom!, + archiveRoom.room, start: 0, limit: Room.defaultHistoryCount, ); @@ -106,10 +74,6 @@ void main() async { test('discard room from archives when membership change', () async { await client.loadArchiveWithTimeline(); - expect( - client.getArchiveRoomFromCache('!5345234235:example.com') != null, - true, - ); await client.handleSync( SyncUpdate( nextBatch: 't_456', @@ -118,85 +82,6 @@ void main() async { ), ), ); - expect(client.getArchiveRoomFromCache('!5345234235:example.com'), null); - }); - - test("assert that key updates don't change membership", () async { - const roomid = '!5345234235:example.com'; - - // prep work to be able to set a last event that would trigger the (fixed) bug - await client.loadArchiveWithTimeline(); - expect(client.getArchiveRoomFromCache(roomid) != null, true); - final room = client.getRoomById(roomid)!; - room.summary.mJoinedMemberCount = 0; - room.summary.mInvitedMemberCount = 0; - expect(room.membership, Membership.leave); - - final outboundSession = await client.encryption?.keyManager - .createOutboundGroupSession(roomid); - final inboundSession = client.encryption!.keyManager - .getInboundGroupSession( - roomid, - outboundSession!.outboundGroupSession!.sessionId, - )!; - - // ensure encryption is "enabled" - client - .getRoomById(roomid) - ?.setState( - StrippedStateEvent( - type: EventTypes.Encryption, - content: {'algorithm': AlgorithmTypes.megolmV1AesSha2}, - senderId: client.userID!, - stateKey: '', - ), - ); - final encryptedEvent = await client.encryption! - .encryptGroupMessagePayload(roomid, { - 'msgtype': 'm.room.text', - 'body': 'empty', - }); - - // reset client - await client.dispose().onError((e, s) {}); - client = await getClient( - sendTimelineEventTimeout: const Duration(seconds: 5), - ); - - await client.abortSync(); - insertList.clear(); - - // now do our tests - await client.loadArchiveWithTimeline(); - expect(client.getArchiveRoomFromCache(roomid) != null, true); - expect(client.getRoomById(roomid)?.membership, Membership.leave); - - // set the last event - room.lastEvent = Event( - type: EventTypes.Encrypted, - content: encryptedEvent, - senderId: client.userID!, - eventId: '\$archivedencr', - room: room, - originServerTs: DateTime.now(), - ); - - // import the inbound session - await client.encryption!.keyManager.setInboundGroupSession( - roomid, - inboundSession.sessionId, - inboundSession.senderKey, - inboundSession.content, - ); - - expect(client.getArchiveRoomFromCache(roomid) != null, true); - expect(client.getRoomById(roomid)?.membership, Membership.leave); - }, tags: 'olm'); - - test('clear archive', () async { - await client.loadArchiveWithTimeline(); - client.clearArchivesFromCache(); - expect(client.getArchiveRoomFromCache('!5345234234:example.com'), null); }); test('logout', () async { From 15002053b3a7f7a10f3f6ba114b2acfb7ed7f406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Fri, 2 Jan 2026 15:31:22 +0100 Subject: [PATCH 2/2] feat: (BREAKING) Store left rooms if syncfilter includes leave --- lib/src/client.dart | 31 ++++++++++++++++++----- lib/src/database/matrix_sdk_database.dart | 6 ++++- lib/src/room.dart | 7 ++++- lib/src/timeline.dart | 6 ++++- test/room_archived_test.dart | 15 ++++++----- 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index e70bfd99e..ce8dd1a65 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -477,8 +477,7 @@ class Client extends MatrixApi { } /// Searches in the local cache for the given room and returns null if not - /// found. If you have loaded the [loadArchive()] before, it can also return - /// archived rooms. + /// found. This does not include archived rooms. Room? getRoomById(String id) { for (final room in rooms) { if (room.id == id) return room; @@ -1282,6 +1281,7 @@ class Client extends MatrixApi { .map((e) => Event.fromMatrixEvent(e, room)) .toList() ?? [], + prevBatch: entry.value.timeline?.prevBatch ?? '', ), ); for (var i = 0; i < timeline.events.length; i++) { @@ -1294,6 +1294,9 @@ class Client extends MatrixApi { } } } + room.lastEvent = timeline.events.firstWhereOrNull( + (event) => roomPreviewLastEvents.contains(event.type), + ); archivedRooms.add(ArchivedRoom(room: room, timeline: timeline)); } @@ -2798,12 +2801,15 @@ class Client extends MatrixApi { room, timelineEvents, timelineUpdateType, - store: false, + store: syncFilter.room?.includeLeave == true, ); } final accountData = syncRoomUpdate.accountData; if (accountData != null && accountData.isNotEmpty) { for (final event in accountData) { + if (syncFilter.room?.includeLeave == true) { + await database.storeRoomAccountData(room.id, event); + } room.roomAccountData[event.type] = event; } } @@ -2813,7 +2819,7 @@ class Client extends MatrixApi { room, state, EventUpdateType.state, - store: false, + store: syncFilter.room?.includeLeave == true, ); } } @@ -3050,7 +3056,14 @@ class Client extends MatrixApi { summary: chatUpdate.summary, client: this, ) - : Room(id: roomId, membership: membership, client: this)); + : Room( + id: roomId, + membership: membership, + prev_batch: chatUpdate is LeftRoomUpdate + ? chatUpdate.timeline?.prevBatch + : null, + client: this, + )); // Does the chat already exist in the list rooms? if (!found && membership != Membership.leave) { @@ -3059,8 +3072,12 @@ class Client extends MatrixApi { rooms.insert(position, room); } // If the membership is "leave" then remove the item and stop here - else if (found && membership == Membership.leave) { - rooms.removeAt(roomIndex); + else if (membership == Membership.leave) { + if (syncFilter.room?.includeLeave == true && !found) { + rooms.add(room); + } else if (found) { + rooms.removeAt(roomIndex); + } } // Update notification, highlight count and/or additional information else if (found && diff --git a/lib/src/database/matrix_sdk_database.dart b/lib/src/database/matrix_sdk_database.dart index fe9f62c95..0c204e156 100644 --- a/lib/src/database/matrix_sdk_database.dart +++ b/lib/src/database/matrix_sdk_database.dart @@ -1264,7 +1264,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { Client client, ) async { // Leave room if membership is leave - if (roomUpdate is LeftRoomUpdate) { + if (roomUpdate is LeftRoomUpdate && + client.syncFilter.room?.includeLeave != true) { await forgetRoom(roomId); return; } @@ -1294,6 +1295,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { : Room( client: client, id: roomId, + prev_batch: roomUpdate is LeftRoomUpdate + ? roomUpdate.timeline?.prevBatch + : null, membership: membership, lastEvent: lastEvent, ).toJson(), diff --git a/lib/src/room.dart b/lib/src/room.dart index 199d9212e..476620386 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -1495,6 +1495,7 @@ class Room { Future forget() async { await client.database.forgetRoom(id); await client.forgetRoom(id); + client.rooms.remove(this); return; } @@ -1772,7 +1773,11 @@ class Room { }); } - var chunk = TimelineChunk(events: events); + var chunk = TimelineChunk( + events: events, + // Leave rooms paginate via getRoomEvents which uses chunk.prevBatch. + prevBatch: isArchived ? (prev_batch ?? '') : '', + ); // Load the timeline arround eventContextId if set if (eventContextId != null) { if (!events.any((Event event) => event.eventId == eventContextId)) { diff --git a/lib/src/timeline.dart b/lib/src/timeline.dart index 420b26365..fb2880896 100644 --- a/lib/src/timeline.dart +++ b/lib/src/timeline.dart @@ -102,6 +102,9 @@ class Timeline { direction: Direction.b, historyCount: historyCount, filter: filter, + inMemoryOnly: + room.membership == Membership.leave && + room.client.syncFilter.room?.includeLeave != true, ); isRequestingHistory = false; } @@ -136,6 +139,7 @@ class Timeline { int historyCount = Room.defaultHistoryCount, required Direction direction, StateFilter? filter, + bool inMemoryOnly = false, }) async { onUpdate?.call(); @@ -184,7 +188,7 @@ class Timeline { _fetchedAllDatabaseEvents = true; Logs().i('No more events found in the store. Request from server...'); - if (isFragmentedTimeline) { + if (isFragmentedTimeline || inMemoryOnly) { await getRoomEvents( historyCount: historyCount, direction: direction, diff --git a/test/room_archived_test.dart b/test/room_archived_test.dart index 4200c9995..d2ad14a4d 100644 --- a/test/room_archived_test.dart +++ b/test/room_archived_test.dart @@ -49,15 +49,18 @@ void main() async { expect(timeline.events.length, 2); expect(timeline.events[0].eventId, '1532735824654:example.org'); expect(timeline.events[1].eventId, '1532735824650:example.org'); + expect(archive.first.room.prev_batch, 't_1234a'); + expect(timeline.chunk.prevBatch, 't_1234a'); await timeline.requestHistory(); - expect(timeline.events.length, 5); - expect(timeline.events[0].eventId, '143274597443PhrSn:example.org'); - expect(timeline.events[1].eventId, '143274597446PhrSn:example.org'); - expect(timeline.events[2].eventId, '3143273582443PhrSn:example.org'); - expect(timeline.events[3].eventId, '2143273582443PhrSn:example.org'); - expect(timeline.events[4].eventId, '1143273582466PhrSn:example.org'); + expect(timeline.events.length, 6); + expect(timeline.events[0].eventId, '1532735824654:example.org'); + expect(timeline.events[1].eventId, '1532735824650:example.org'); + expect(timeline.events[2].eventId, '1432735824656:example.org'); + expect(timeline.events[3].eventId, '1432735824655:example.org'); + expect(timeline.events[4].eventId, '1432735824654:example.org'); + expect(timeline.events[5].eventId, '1432735824653:example.org'); }); test('expect database to be empty', () async {