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(