diff --git a/lib/src/client.dart b/lib/src/client.dart index 38a0195d..b4f6c7bc 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -2776,13 +2776,13 @@ class Client extends MatrixApi { final List receipts = []; for (final event in events) { + room.setEphemeral(event); // Receipt events are deltas between two states. We will create a // fake room account data event for this and store the difference // there. if (event.type != 'm.receipt') continue; - receipts.add(ReceiptEventContent.fromJson(event.content)); } @@ -2797,6 +2797,7 @@ class Client extends MatrixApi { type: LatestReceiptState.eventType, content: receiptStateContent.toJson(), ); + await database.storeRoomAccountData(room.id, event); room.roomAccountData[event.type] = event; } diff --git a/lib/src/database/database_api.dart b/lib/src/database/database_api.dart index 509cf55c..5a56d22f 100644 --- a/lib/src/database/database_api.dart +++ b/lib/src/database/database_api.dart @@ -68,6 +68,8 @@ abstract class DatabaseApi { Event threadRootEvent, Event? lastEvent, bool currentUserParticipated, + int? notificationCount, + int? highlightCount, int count, Client client, ); diff --git a/lib/src/database/matrix_sdk_database.dart b/lib/src/database/matrix_sdk_database.dart index 317f8b75..3f0263c6 100644 --- a/lib/src/database/matrix_sdk_database.dart +++ b/lib/src/database/matrix_sdk_database.dart @@ -1401,6 +1401,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { Event threadRootEvent, Event? lastEvent, bool currentUserParticipated, + int? notificationCount, + int? highlightCount, int count, Client client, ) async { @@ -1415,6 +1417,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { client: client, currentUserParticipated: currentUserParticipated, count: count, + notificationCount: notificationCount ?? 0, + highlightCount: highlightCount ?? 0, ).toJson(), ); } diff --git a/lib/src/room.dart b/lib/src/room.dart index 5b5e1c8b..12b218bb 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -26,7 +26,6 @@ import 'package:html_unescape/html_unescape.dart'; import 'package:matrix/matrix.dart'; import 'package:matrix/src/models/timeline_chunk.dart'; -import 'package:matrix/src/room_timeline.dart'; import 'package:matrix/src/utils/cached_stream_controller.dart'; import 'package:matrix/src/utils/file_send_request_credentials.dart'; import 'package:matrix/src/utils/markdown.dart'; @@ -148,17 +147,18 @@ class Room { for (final threadEvent in response.chunk) { final event = Event.fromMatrixEvent(threadEvent, this); + final thread = Thread.fromJson(threadEvent.toJson(), client); // Store thread in database await client.database.storeThread( id, event, - event, // lastEvent - false, // currentUserParticipated - 1, // count + thread.lastEvent, // lastEvent + thread.currentUserParticipated ?? false, // currentUserParticipated + 0, 0, + thread.count ?? 1, // count client, ); - threads[event.eventId] = - (await client.database.getThread(id, event.eventId, client))!; + threads[event.eventId] = thread; } if (response.nextBatch == null) { @@ -180,12 +180,14 @@ class Room { // Update thread metadata in database final root = await getEventById(event.relationshipEventId!); if (root == null) return; + final thread = await client.database.getThread(id, event.relationshipEventId!, client); await client.database.storeThread( id, root, event, // update last event event.senderId == client.userID, // currentUserParticipated - 1, // increment count - should be calculated properly + (thread?.count ?? 0) + 1, // increment count - should be calculated properly + 0, 0, client, ); } @@ -243,14 +245,17 @@ class Room { Future getThread(Event rootEvent) async { final threads = await getThreads(); - if (threads.containsKey(rootEvent.eventId)) + if (threads.containsKey(rootEvent.eventId)) { return threads[rootEvent.eventId]!; + } return Thread( room: this, rootEvent: rootEvent, client: client, currentUserParticipated: false, count: 0, + highlightCount: 0, + notificationCount: 0, ); } @@ -1523,7 +1528,6 @@ class Room { direction = Direction.b, StateFilter? filter, }) async { - unawaited(_loadThreadsFromServer()); final prev_batch = this.prev_batch; diff --git a/lib/src/thread.dart b/lib/src/thread.dart index 7e99270b..726a48c9 100644 --- a/lib/src/thread.dart +++ b/lib/src/thread.dart @@ -1,5 +1,6 @@ +import 'dart:async'; + import 'package:matrix/matrix.dart'; -import 'package:matrix/matrix_api_lite/generated/internal.dart'; import 'package:matrix/src/models/timeline_chunk.dart'; class Thread { @@ -11,16 +12,28 @@ class Thread { int? count; final Client client; + /// The count of unread notifications. + int notificationCount = 0; + + /// The count of highlighted notifications. + int highlightCount = 0; + Thread({ required this.room, required this.rootEvent, required this.client, required this.currentUserParticipated, required this.count, + required this.notificationCount, + required this.highlightCount, this.prev_batch, this.lastEvent, }); + /// Returns true if this room is unread. To check if there are new messages + /// in muted rooms, use [hasNewMessages]. + bool get isUnread => notificationCount > 0; + Map toJson() => { ...rootEvent.toJson(), 'unsigned': { @@ -66,10 +79,20 @@ class Thread { count: json['unsigned']?['m.relations']?['m.thread']?['count'], currentUserParticipated: json['unsigned']?['m.relations']?['m.thread'] ?['current_user_participated'], + highlightCount: 0, + notificationCount: 0, ); return thread; } + Future refreshLastEvent({ + timeout = const Duration(seconds: 30), + }) async { + final lastEvent = _refreshingLastEvent ??= _refreshLastEvent(); + _refreshingLastEvent = null; + return lastEvent; + } + Future? _refreshingLastEvent; Future _refreshLastEvent({ @@ -121,8 +144,27 @@ class Thread { } bool get hasNewMessages { - // TODO: Implement this - return false; + final lastEvent = this.lastEvent; + + // There is no known event or the last event is only a state fallback event, + // we assume there is no new messages. + if (lastEvent == null || + !client.roomPreviewLastEvents.contains(lastEvent.type)) { + return false; + } + + // Read marker is on the last event so no new messages. + if (lastEvent.receipts + .any((receipt) => receipt.user.senderId == client.userID!)) { + return false; + } + + // If the last event is sent, we mark the room as read. + if (lastEvent.senderId == client.userID) return false; + + // Get the timestamp of read marker and compare + final readAtMilliseconds = room.receiptState.byThread[rootEvent.eventId]?.latestOwnReceipt?.ts ?? 0; + return readAtMilliseconds < lastEvent.originServerTs.millisecondsSinceEpoch; } Future getEventContext(String eventId) async { @@ -327,6 +369,22 @@ class Thread { ); } + Future setLastEvent(Event event) async { + lastEvent = event; + final thread = await client.database.getThread(room.id, rootEvent.eventId, client); + Logs().v('Set lastEvent to ${room.id}:${rootEvent.eventId} (${event.senderId})'); + await client.database.storeThread( + room.id, + rootEvent, + lastEvent, + currentUserParticipated ?? false, + notificationCount, + highlightCount, + (thread?.count ?? 0) + 1, + client, + ); + } + Future requestHistory({ int historyCount = Room.defaultHistoryCount, void Function()? onHistoryReceived, diff --git a/lib/src/thread_timeline.dart b/lib/src/thread_timeline.dart index 4aa4fc08..7772c3d3 100644 --- a/lib/src/thread_timeline.dart +++ b/lib/src/thread_timeline.dart @@ -119,6 +119,8 @@ class ThreadTimeline extends Timeline { addAggregatedEvent(event); } + unawaited(thread.setLastEvent(events[events.length - 1])); + // Handle redaction events if (event.type == EventTypes.Redaction) { final index = _findEvent(event_id: event.redacts); @@ -428,14 +430,15 @@ class ThreadTimeline extends Timeline { } @override - Stream<(List, String?)> startSearch( - {String? searchTerm, - int requestHistoryCount = 100, - int maxHistoryRequests = 10, - String? prevBatch, - String? sinceEventId, - int? limit, - bool Function(Event p1)? searchFunc}) { + Stream<(List, String?)> startSearch({ + String? searchTerm, + int requestHistoryCount = 100, + int maxHistoryRequests = 10, + String? prevBatch, + String? sinceEventId, + int? limit, + bool Function(Event p1)? searchFunc, + }) { // TODO: implement startSearch throw UnimplementedError(); } @@ -466,10 +469,10 @@ class ThreadTimeline extends Timeline { } @override - void requestKeys({ + Future requestKeys({ bool tryOnlineBackup = true, bool onlineKeyBackupOnly = true, - }) { + }) async { for (final event in events) { if (event.type == EventTypes.Encrypted && event.messageType == MessageTypes.BadEncrypted && @@ -477,7 +480,7 @@ class ThreadTimeline extends Timeline { final sessionId = event.content.tryGet('session_id'); final senderKey = event.content.tryGet('sender_key'); if (sessionId != null && senderKey != null) { - thread.room.requestSessionKey(sessionId, senderKey); + await thread.room.requestSessionKey(sessionId, senderKey); } } }