From 8a4eda5201a969da6d261cc028bc2110bfba4695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 10 Sep 2025 13:55:21 +0200 Subject: [PATCH] feat: Auto refresh last event after limited timeline This adds a new behavior on sync that the app automatically fetches the last event from server after we receive a limited timeline and the lastEvent has not changed. --- lib/fake_matrix_api.dart | 22 ++++++++++++++++++ lib/src/client.dart | 17 +++++++------- lib/src/room.dart | 49 ++++++++++++++++++++++++++++++++++++++++ test/room_test.dart | 23 +++++++++++++++++++ 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/lib/fake_matrix_api.dart b/lib/fake_matrix_api.dart index f3281824..a37b70a6 100644 --- a/lib/fake_matrix_api.dart +++ b/lib/fake_matrix_api.dart @@ -1740,6 +1740,28 @@ class FakeMatrixApi extends BaseClient { 'origin_server_ts': 1432735824653, 'unsigned': {'age': 1234}, }, + '/client/v3/rooms/!localpart%3Aserver.abc/messages?dir=b&limit=1&filter=%7B%22types%22%3A%5B%22m.room.message%22%2C%22m.room.encrypted%22%2C%22m.sticker%22%2C%22m.call.invite%22%2C%22m.call.answer%22%2C%22m.call.reject%22%2C%22m.call.hangup%22%2C%22com.famedly.call.member%22%5D%7D': + (var req) => { + 'start': 't47429-4392820_219380_26003_2265', + 'end': 't47409-4357353_219380_26003_2265', + 'chunk': [ + { + 'content': { + 'body': 'This is an example text message', + 'msgtype': 'm.text', + 'format': 'org.matrix.custom.html', + 'formatted_body': + 'This is an example text message', + }, + 'type': 'm.room.message', + 'event_id': '3143273582443PhrSn:example.org', + 'room_id': '!1234:example.com', + 'sender': '@example:example.org', + 'origin_server_ts': 1432735824653, + 'unsigned': {'age': 1234}, + }, + ], + }, '/client/v3/rooms/new_room_id/messages?from=emptyHistoryResponse&dir=b&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D': (var req) => emptyHistoryResponse, '/client/v3/rooms/new_room_id/messages?from=1&dir=b&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D': diff --git a/lib/src/client.dart b/lib/src/client.dart index 22a7b922..a2e493e0 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -2632,13 +2632,15 @@ class Client extends MatrixApi { final id = entry.key; final syncRoomUpdate = entry.value; + final room = await _updateRoomsByRoomUpdate(id, syncRoomUpdate); + // 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); + room.lastEvent = null; } - final room = await _updateRoomsByRoomUpdate(id, syncRoomUpdate); final timelineUpdateType = direction != null ? (direction == Direction.b @@ -2739,6 +2741,12 @@ class Client extends MatrixApi { continue; } await database.storeRoomUpdate(id, syncRoomUpdate, room.lastEvent, this); + + if (syncRoomUpdate is JoinedRoomUpdate && + syncRoomUpdate.timeline?.limited == true && + room.lastEvent == null) { + runInRoot(room.refreshLastEvent); + } } } @@ -3035,13 +3043,6 @@ class Client extends MatrixApi { } if (type != EventUpdateType.timeline) break; - // If last event is null or not a valid room preview event anyway, - // just use this: - if (room.lastEvent == null) { - room.lastEvent = event; - break; - } - // Is this event redacting the last event? if (event.type == EventTypes.Redaction && ({ diff --git a/lib/src/room.dart b/lib/src/room.dart index 86ec752d..91cd80e7 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -377,6 +377,55 @@ class Room { Event? lastEvent; + /// Fetches the most recent event in the timeline from the server to have + /// a valid preview after receiving a limited timeline from the sync. Will + /// be triggered by the sync loop on demand. + Future refreshLastEvent() async { + if (membership != Membership.join) return null; + + final filter = StateFilter(types: client.roomPreviewLastEvents.toList()); + final result = await client.getRoomEvents( + id, + Direction.b, + limit: 1, + filter: jsonEncode(filter.toJson()), + ); + final matrixEvent = result.chunk.firstOrNull; + if (matrixEvent == null) { + Logs().d('No last event found for room', id); + return null; + } + var event = Event.fromMatrixEvent( + matrixEvent, + this, + status: EventStatus.synced, + ); + if (event.type == EventTypes.Encrypted) { + final encryption = client.encryption; + if (encryption != null) { + event = await encryption.decryptRoomEvent(event); + } + } + lastEvent = event; + + Logs().d('Refreshed last event for room', id); + + // Trigger sync handling so that lastEvent gets stored and room list gets + // updated. + await _handleFakeSync( + SyncUpdate( + nextBatch: '', + rooms: RoomsUpdate( + join: { + id: JoinedRoomUpdate(timeline: TimelineUpdate(limited: false)), + }, + ), + ), + ); + + return event; + } + void setEphemeral(BasicEvent ephemeral) { ephemerals[ephemeral.type] = ephemeral; if (ephemeral.type == 'm.typing') { diff --git a/test/room_test.dart b/test/room_test.dart index e6963a4c..01fe4d20 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -883,6 +883,29 @@ void main() { expect(timeline.events.length, 17); }); + test('Refresh last event', () async { + expect(room.lastEvent?.eventId, '12'); + final lastEventUpdate = + room.client.onSync.stream.firstWhere((u) => u.nextBatch.isEmpty); + await room.client.handleSync( + SyncUpdate( + nextBatch: 'abcd', + rooms: RoomsUpdate( + join: { + room.id: JoinedRoomUpdate( + timeline: TimelineUpdate( + events: [], + limited: true, + ), + ), + }, + ), + ), + ); + await lastEventUpdate; + expect(room.lastEvent?.eventId, '3143273582443PhrSn:example.org'); + }); + test('isFederated', () { expect(room.isFederated, true); room.setState(