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/matrix_api_lite/model/event_types.dart b/lib/matrix_api_lite/model/event_types.dart index 0e39bb5d..b99282ea 100644 --- a/lib/matrix_api_lite/model/event_types.dart +++ b/lib/matrix_api_lite/model/event_types.dart @@ -61,6 +61,10 @@ abstract class EventTypes { 'org.matrix.call.asserted_identity'; static const String Unknown = 'm.unknown'; + /// An internal event type indicating that the last event in the room for + /// a room list preview is currently being refreshed. + static const String refreshingLastEvent = 'com.famedly.refreshing_last_event'; + // To device event types static const String RoomKey = 'm.room_key'; static const String ForwardedRoomKey = 'm.forwarded_room_key'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 22a7b922..5d0c0453 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 @@ -2738,6 +2740,24 @@ class Client extends MatrixApi { Logs().d('Skip store LeftRoomUpdate for unknown room', id); continue; } + + if (syncRoomUpdate is JoinedRoomUpdate && + (room.lastEvent?.type == EventTypes.refreshingLastEvent || + (syncRoomUpdate.timeline?.limited == true && + room.lastEvent == null))) { + room.lastEvent = Event( + originServerTs: + syncRoomUpdate.timeline?.events?.firstOrNull?.originServerTs ?? + DateTime.now(), + type: EventTypes.refreshingLastEvent, + content: {'body': 'Refreshing last event...'}, + room: room, + eventId: generateUniqueTransactionId(), + senderId: userID!, + ); + runInRoot(room.refreshLastEvent); + } + await database.storeRoomUpdate(id, syncRoomUpdate, room.lastEvent, this); } } @@ -3035,13 +3055,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..dd7c6825 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -377,6 +377,73 @@ 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. Multiple requests will be + /// combined to the same request. + Future refreshLastEvent({ + timeout = const Duration(seconds: 30), + }) async { + final lastEvent = _refreshingLastEvent ??= _refreshLastEvent(); + _refreshingLastEvent = null; + return lastEvent; + } + + Future? _refreshingLastEvent; + + Future _refreshLastEvent({ + timeout = const Duration(seconds: 30), + }) 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()), + ) + .timeout(timeout); + final matrixEvent = result.chunk.firstOrNull; + if (matrixEvent == null) { + if (lastEvent?.type == EventTypes.refreshingLastEvent) { + lastEvent = 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') { @@ -443,8 +510,16 @@ class Room { String get displayname => getLocalizedDisplayname(); /// When was the last event received. - DateTime get latestEventReceivedTime => - lastEvent?.originServerTs ?? DateTime.now(); + DateTime get latestEventReceivedTime { + final lastEventTime = lastEvent?.originServerTs; + if (lastEventTime != null) return lastEventTime; + + if (membership == Membership.invite) return DateTime.now(); + final createEvent = getState(EventTypes.RoomCreate); + if (createEvent is MatrixEvent) return createEvent.originServerTs; + + return DateTime(0); + } /// Call the Matrix API to change the name of this room. Returns the event ID of the /// new m.room.name event. diff --git a/lib/src/utils/event_localizations.dart b/lib/src/utils/event_localizations.dart index a4fb76d8..4f19972e 100644 --- a/lib/src/utils/event_localizations.dart +++ b/lib/src/utils/event_localizations.dart @@ -299,5 +299,6 @@ abstract class EventLocalizations { ?.tryGet('key') ?? body, ), + EventTypes.refreshingLastEvent: (_, i18n, ___) => i18n.refreshingLastEvent, }; } diff --git a/lib/src/utils/matrix_default_localizations.dart b/lib/src/utils/matrix_default_localizations.dart index 9f5efc58..c9076777 100644 --- a/lib/src/utils/matrix_default_localizations.dart +++ b/lib/src/utils/matrix_default_localizations.dart @@ -318,4 +318,7 @@ class MatrixDefaultLocalizations extends MatrixLocalizations { : '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')} '; return '$senderName: ${durationString}Voice message'; } + + @override + String get refreshingLastEvent => 'Refreshing last event...'; } diff --git a/lib/src/utils/matrix_localizations.dart b/lib/src/utils/matrix_localizations.dart index 921747f0..37c9672f 100644 --- a/lib/src/utils/matrix_localizations.dart +++ b/lib/src/utils/matrix_localizations.dart @@ -62,6 +62,8 @@ abstract class MatrixLocalizations { String get cancelledSend; + String get refreshingLastEvent; + String youInvitedBy(String senderName); String invitedBy(String senderName); diff --git a/test/client_test.dart b/test/client_test.dart index e38378a9..19773a1e 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -201,26 +201,26 @@ void main() { matrix.getDirectChatFromUserId('@bob:example.com'), '!726s6s6q:example.com', ); - expect(matrix.rooms[2].directChatMatrixID, '@bob:example.com'); + expect(matrix.rooms[1].directChatMatrixID, '@bob:example.com'); expect(matrix.directChats, matrix.accountData['m.direct']?.content); // ignore: deprecated_member_use_from_same_package expect(matrix.presences.length, 1); - expect(matrix.rooms[2].ephemerals.length, 2); - expect(matrix.rooms[2].typingUsers.length, 1); - expect(matrix.rooms[2].typingUsers[0].id, '@alice:example.com'); - expect(matrix.rooms[2].roomAccountData.length, 3); - expect(matrix.rooms[2].encrypted, true); + expect(matrix.rooms[1].ephemerals.length, 2); + expect(matrix.rooms[1].typingUsers.length, 1); + expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com'); + expect(matrix.rooms[1].roomAccountData.length, 3); + expect(matrix.rooms[1].encrypted, true); expect( - matrix.rooms[2].encryptionAlgorithm, + matrix.rooms[1].encryptionAlgorithm, Client.supportedGroupEncryptionAlgorithms.first, ); expect( matrix - .rooms[2].receiptState.global.otherUsers['@alice:example.com']?.ts, + .rooms[1].receiptState.global.otherUsers['@alice:example.com']?.ts, 1436451550453, ); expect( - matrix.rooms[2].receiptState.global.otherUsers['@alice:example.com'] + matrix.rooms[1].receiptState.global.otherUsers['@alice:example.com'] ?.eventId, '\$7365636s6r6432:example.com', ); @@ -231,7 +231,7 @@ void main() { expect(inviteRoom.states[EventTypes.RoomMember]?.length, 1); expect(matrix.rooms.length, 3); expect( - matrix.rooms[2].canonicalAlias, + matrix.rooms[1].canonicalAlias, "#famedlyContactDiscovery:${matrix.userID!.split(":")[1]}", ); expect( 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(