Skip to content

Krille/refactor-archive-room-handling#2217

Open
krille-chan wants to merge 2 commits into
mainfrom
krille/refactor-archive-room-handling
Open

Krille/refactor-archive-room-handling#2217
krille-chan wants to merge 2 commits into
mainfrom
krille/refactor-archive-room-handling

Conversation

@krille-chan

@krille-chan krille-chan commented Jan 2, 2026

Copy link
Copy Markdown
Contributor

closes #2031

Test out with:

Client(
  // ...
 syncFilter: Filter(
    room: RoomFilter(
      includeLeave: true,
      state: StateFilter(lazyLoadMembers: true),
    ),
  ),
);

If you don't change the syncFilter, the behavior should be as before, just that you now have to cache the archive by yourself when requesting it.

@krille-chan krille-chan force-pushed the krille/refactor-archive-room-handling branch from 7320e86 to 44c54ee Compare January 2, 2026 14:32
@codecov

codecov Bot commented Jan 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.22%. Comparing base (f9e6b68) to head (8bb6523).

Files with missing lines Patch % Lines
lib/src/client.dart 76.47% 8 Missing ⚠️
lib/src/database/matrix_sdk_database.dart 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2217      +/-   ##
==========================================
- Coverage   59.29%   59.22%   -0.08%     
==========================================
  Files         161      161              
  Lines       20300    20280      -20     
==========================================
- Hits        12037    12010      -27     
- Misses       8263     8270       +7     
Files with missing lines Coverage Δ
lib/src/room.dart 76.52% <ø> (+0.24%) ⬆️
lib/src/database/matrix_sdk_database.dart 90.19% <50.00%> (-1.54%) ⬇️
lib/src/client.dart 78.11% <76.47%> (-0.05%) ⬇️

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f9e6b68...8bb6523. Read the comment docs.

@krille-chan krille-chan force-pushed the krille/refactor-archive-room-handling branch from 44c54ee to 129b959 Compare May 15, 2026 06:52
@krille-chan krille-chan force-pushed the krille/refactor-archive-room-handling branch 2 times, most recently from 44634d1 to 84eccf8 Compare June 19, 2026 13:24
@krille-chan krille-chan marked this pull request as ready for review June 19, 2026 13:24
Comment thread lib/src/client.dart Outdated
Comment thread lib/src/client.dart
@krille-chan krille-chan force-pushed the krille/refactor-archive-room-handling branch 3 times, most recently from 4f2d4cf to 8b3c7bf Compare June 26, 2026 08:45
@krille-chan krille-chan force-pushed the krille/refactor-archive-room-handling branch from 8b3c7bf to 8bb6523 Compare July 6, 2026 06:33
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Breaking API for archive getters/cache helpers and a behavior shift when includeLeave is enabled (left rooms in rooms and DB). Default sync behavior should match prior leave handling.

Overview
Removes the SDK’s in-memory archived-room cache (archivedRooms, getArchiveRoomFromCache, clearArchivesFromCache, and automatic archive updates on leave/forget). loadArchive() / loadArchiveWithTimeline() still fetch left rooms via a dedicated sync, but results are returned only from that call—callers must keep their own cache if needed.

Left-room handling is now driven by Client.syncFilter: with default filters, leaving still drops the room from the in-memory list and forgetRoom in the database (unchanged). If room.includeLeave: true, sync persists left-room timeline/state/account data, storeRoomUpdate no longer deletes the room on leave, and left rooms can appear in client.rooms with Membership.leave.

getRoomById and Room.getTimeline no longer resolve archived rooms from the old cache; archived (leave) rooms skip loading events from the DB in getTimeline unless the app loaded history another way.

Reviewed by Cursor Bugbot for commit 8bb6523. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Leave removes room with includeLeave
    • Left rooms are now retained in client.rooms with leave membership and prev_batch when includeLeave is enabled.
  • ✅ Fixed: Archived getTimeline skips database
    • getTimeline now reads persisted events from the database for archived rooms as well as active rooms.
  • ✅ Fixed: Leave update skips DB membership
    • storeRoomUpdate now updates existing room rows for leave updates, including membership, prev_batch, and lastEvent.
  • ✅ Fixed: Re-invite skips membership update
    • Existing rooms now update their in-memory and persisted membership when an invite update arrives.

Create PR

Or push these changes by commenting:

@cursor push 20b0a05559
Preview (20b0a05559)
diff --git a/lib/src/client.dart b/lib/src/client.dart
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -2983,12 +2983,26 @@
     }
     // If the membership is "leave" then remove the item and stop here
     else if (membership == Membership.leave) {
-      if (syncFilter.room?.includeLeave == true && !found) {
-        rooms.add(room);
+      final leftUpdate = chatUpdate as LeftRoomUpdate;
+      if (syncFilter.room?.includeLeave == true) {
+        final membershipChanged = found && room.membership != membership;
+        room.membership = membership;
+        room.prev_batch = leftUpdate.timeline?.prevBatch ?? room.prev_batch;
+        if (!found) rooms.add(room);
+        if (membershipChanged) {
+          // ignore: deprecated_member_use_from_same_package
+          room.onUpdate.add(room.id);
+        }
       } else if (found) {
         rooms.removeAt(roomIndex);
       }
     }
+    // Update membership for existing invites.
+    else if (found && chatUpdate is InvitedRoomUpdate) {
+      rooms[roomIndex].membership = membership;
+      // ignore: deprecated_member_use_from_same_package
+      rooms[roomIndex].onUpdate.add(rooms[roomIndex].id);
+    }
     // Update notification, highlight count and/or additional information
     else if (found &&
         chatUpdate is JoinedRoomUpdate &&

diff --git a/lib/src/database/matrix_sdk_database.dart b/lib/src/database/matrix_sdk_database.dart
--- a/lib/src/database/matrix_sdk_database.dart
+++ b/lib/src/database/matrix_sdk_database.dart
@@ -1305,8 +1305,10 @@
                 lastEvent: lastEvent,
               ).toJson(),
       );
-    } else if (roomUpdate is JoinedRoomUpdate) {
+    } else {
       final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
+      final joinedUpdate = roomUpdate is JoinedRoomUpdate ? roomUpdate : null;
+      final leftUpdate = roomUpdate is LeftRoomUpdate ? roomUpdate : null;
       await _roomsBox.put(
         roomId,
         Room(
@@ -1314,17 +1316,22 @@
           id: roomId,
           membership: membership,
           highlightCount:
-              roomUpdate.unreadNotifications?.highlightCount ??
+              joinedUpdate?.unreadNotifications?.highlightCount ??
               currentRoom.highlightCount,
           notificationCount:
-              roomUpdate.unreadNotifications?.notificationCount ??
+              joinedUpdate?.unreadNotifications?.notificationCount ??
               currentRoom.notificationCount,
-          prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
-          summary: RoomSummary.fromJson(
-            currentRoom.summary.toJson()
-              ..addAll(roomUpdate.summary?.toJson() ?? {}),
-          ),
-          lastEvent: lastEvent,
+          prev_batch:
+              joinedUpdate?.timeline?.prevBatch ??
+              leftUpdate?.timeline?.prevBatch ??
+              currentRoom.prev_batch,
+          summary: joinedUpdate == null
+              ? currentRoom.summary
+              : RoomSummary.fromJson(
+                  currentRoom.summary.toJson()
+                    ..addAll(joinedUpdate.summary?.toJson() ?? {}),
+                ),
+          lastEvent: lastEvent ?? currentRoom.lastEvent,
         ).toJson(),
       );
     }

diff --git a/lib/src/room.dart b/lib/src/room.dart
--- a/lib/src/room.dart
+++ b/lib/src/room.dart
@@ -1701,11 +1701,9 @@
 
     var events = <Event>[];
 
-    if (!isArchived) {
-      await client.database.transaction(() async {
-        events = await client.database.getEventList(this, limit: limit);
-      });
-    }
+    await client.database.transaction(() async {
+      events = await client.database.getEventList(this, limit: limit);
+    });
 
     var chunk = TimelineChunk(events: events);
     // Load the timeline arround eventContextId if set

diff --git a/test/fake_client.dart b/test/fake_client.dart
--- a/test/fake_client.dart
+++ b/test/fake_client.dart
@@ -20,6 +20,7 @@
 Future<Client> getClient({
   Duration sendTimelineEventTimeout = const Duration(minutes: 1),
   String? databasePath,
+  Filter? syncFilter,
 }) async {
   try {
     vodInit ??= vod.init(
@@ -37,6 +38,7 @@
     database: await getDatabase(databasePath: databasePath),
     onSoftLogout: (client) => client.refreshAccessToken(),
     sendTimelineEventTimeout: sendTimelineEventTimeout,
+    syncFilter: syncFilter,
   );
   FakeMatrixApi.client = client;
   await client.checkHomeserver(

diff --git a/test/room_archived_test.dart b/test/room_archived_test.dart
--- a/test/room_archived_test.dart
+++ b/test/room_archived_test.dart
@@ -73,6 +73,122 @@
       expect(eventsFromStore.isEmpty, true);
     });
 
+    test('includeLeave keeps left room in rooms and database', () async {
+      await client.dispose().onError((e, s) {});
+      client = await getClient(
+        syncFilter: Filter(
+          room: RoomFilter(
+            state: StateFilter(lazyLoadMembers: true),
+            includeLeave: true,
+          ),
+        ),
+      );
+      client.rooms.clear();
+      await client.database.clearCache();
+
+      const roomId = '!includeLeaveRoom:example.com';
+      await client.handleSync(
+        SyncUpdate(
+          nextBatch: 't_join',
+          rooms: RoomsUpdate(
+            join: {
+              roomId: JoinedRoomUpdate(
+                timeline: TimelineUpdate(
+                  prevBatch: 'join_batch',
+                  events: [
+                    MatrixEvent(
+                      type: EventTypes.Message,
+                      senderId: '@alice:example.com',
+                      eventId: '\$include-leave-join',
+                      originServerTs: DateTime.fromMillisecondsSinceEpoch(1),
+                      content: {'msgtype': 'm.text', 'body': 'joined'},
+                    ),
+                  ],
+                ),
+              ),
+            },
+          ),
+        ),
+      );
+
+      await client.handleSync(
+        SyncUpdate(
+          nextBatch: 't_leave',
+          rooms: RoomsUpdate(
+            leave: {
+              roomId: LeftRoomUpdate(
+                timeline: TimelineUpdate(
+                  prevBatch: 'leave_batch',
+                  events: [
+                    MatrixEvent(
+                      type: EventTypes.Message,
+                      senderId: '@alice:example.com',
+                      eventId: '\$include-leave-left',
+                      originServerTs: DateTime.fromMillisecondsSinceEpoch(2),
+                      content: {'msgtype': 'm.text', 'body': 'left'},
+                    ),
+                  ],
+                ),
+              ),
+            },
+          ),
+        ),
+      );
+
+      final room = client.getRoomById(roomId);
+      expect(room, isNotNull);
+      expect(room!.membership, Membership.leave);
+      expect(room.prev_batch, 'leave_batch');
+
+      final storedRoom = await client.database.getSingleRoom(client, roomId);
+      expect(storedRoom?.membership, Membership.leave);
+      expect(storedRoom?.prev_batch, 'leave_batch');
+      expect(storedRoom?.lastEvent?.eventId, '\$include-leave-left');
+
+      final timeline = await room.getTimeline();
+      expect(
+        timeline.events.map((event) => event.eventId),
+        contains('\$include-leave-left'),
+      );
+    });
+
+    test('reinvite updates kept left room membership', () async {
+      await client.dispose().onError((e, s) {});
+      client = await getClient(
+        syncFilter: Filter(
+          room: RoomFilter(
+            state: StateFilter(lazyLoadMembers: true),
+            includeLeave: true,
+          ),
+        ),
+      );
+      client.rooms.clear();
+      await client.database.clearCache();
+
+      const roomId = '!reinviteLeftRoom:example.com';
+      await client.handleSync(
+        SyncUpdate(
+          nextBatch: 't_join',
+          rooms: RoomsUpdate(join: {roomId: JoinedRoomUpdate()}),
+        ),
+      );
+      await client.handleSync(
+        SyncUpdate(
+          nextBatch: 't_leave',
+          rooms: RoomsUpdate(leave: {roomId: LeftRoomUpdate()}),
+        ),
+      );
+
+      await client.handleSync(
+        SyncUpdate(
+          nextBatch: 't_invite',
+          rooms: RoomsUpdate(invite: {roomId: InvitedRoomUpdate()}),
+        ),
+      );
+
+      expect(client.getRoomById(roomId)?.membership, Membership.invite);
+    });
+
     test('discard room from archives when membership change', () async {
       await client.loadArchiveWithTimeline();
       await client.handleSync(

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 8bb6523. Configure here.

Comment thread lib/src/client.dart
if (syncFilter.room?.includeLeave == true && !found) {
rooms.add(room);
} else if (found) {
rooms.removeAt(roomIndex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave removes room with includeLeave

High Severity

When a user leaves a room they were previously joined to, the room is incorrectly removed from client.rooms. This prevents the new includeLeave logic from persisting the left room's state in the database and causes getRoomById to fail for these rooms, potentially leaving stale local data.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8bb6523. Configure here.

Comment thread lib/src/room.dart
await client.database.transaction(() async {
events = await client.database.getEventList(this, limit: limit);
});
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Archived getTimeline skips database

High Severity

The getTimeline method no longer loads events for isArchived (left) rooms. The removed else branch previously handled fetching these, but now the database query only runs for non-archived rooms. This results in empty timelines for left rooms, even when events are locally persisted from includeLeave sync.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8bb6523. Configure here.

// Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) {
if (roomUpdate is LeftRoomUpdate &&
client.syncFilter.room?.includeLeave != true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave update skips DB membership

Medium Severity

When storeRoomUpdate processes a LeftRoomUpdate with client.syncFilter.room?.includeLeave enabled, it doesn't correctly update the existing room's membership to leave or persist lastEvent and prev_batch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8bb6523. Configure here.

Comment thread lib/src/client.dart
if (syncFilter.room?.includeLeave == true && !found) {
rooms.add(room);
} else if (found) {
rooms.removeAt(roomIndex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-invite skips membership update

Medium Severity

When a room is already in client.rooms (e.g. a left room kept via includeLeave), an InvitedRoomUpdate does not update rooms[roomIndex].membership to invite because membership changes run only in the JoinedRoomUpdate branch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8bb6523. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make it possible to keep leave rooms when syncfilter includes them

1 participant