import 'package:matrix/matrix.dart'; /// Read receipts per thread (MSC3771, in the spec since v1.4): a receipt /// with a `thread_id` says how far someone has read in that thread alone, /// so reading a thread doesn't mark the room read, nor the room a thread. /// The SDK keeps them apart already (`receiptState.byThread`) but only /// sends unthreaded ones; the thread screen sends these. /// Our receipt in the thread under [rootId], public or private, whichever /// is further. LatestReceiptStateData? ownThreadReceipt(Room room, String rootId) => room.receiptState.byThread[rootId]?.latestOwnReceipt; /// Marks the thread under [rootId] read up to [eventId], a reply in it. /// The private receipt always; the public one too unless the user keeps /// receipts private, as for the room. Future markThreadRead(Room room, String rootId, String eventId) async { if (ownThreadReceipt(room, rootId)?.eventId == eventId) return; final client = room.client; await client.postReceipt( room.id, ReceiptType.mReadPrivate, eventId, threadId: rootId, ); if (client.receiptsPublicByDefault) { await client.postReceipt( room.id, ReceiptType.mRead, eventId, threadId: rootId, ); } } /// The other people whose receipt in the thread under [rootId] is on /// [eventId], with those whose unthreaded receipt is. Set threadReaders(Room room, String rootId, String eventId) { final me = room.client.userID; final state = room.receiptState; return { for (final e in state.byThread[rootId]?.otherUsers.entries ?? const >[]) if (e.value.eventId == eventId && e.key != me) e.key, for (final e in state.global.otherUsers.entries) if (e.value.eventId == eventId && e.key != me) e.key, }; } /// The newest reply in a thread, as far as this device knows: the /// server's summary on the root, or a newer one that came in since. ({String eventId, String senderId, DateTime time})? latestThreadReply( Event root, [ Timeline? timeline, ]) { ({String eventId, String senderId, DateTime time})? latest; final bundled = root.unsigned ?.tryGetMap('m.relations') ?.tryGetMap(RelationshipTypes.thread) ?.tryGetMap('latest_event'); final id = bundled?.tryGet('event_id'); final sender = bundled?.tryGet('sender'); final ts = bundled?.tryGet('origin_server_ts'); if (id != null && sender != null && ts != null) { latest = ( eventId: id, senderId: sender, time: DateTime.fromMillisecondsSinceEpoch(ts), ); } if (timeline != null) { for (final e in root.aggregatedEvents(timeline, RelationshipTypes.thread)) { if (!e.status.isSynced) continue; if (latest == null || e.originServerTs.isAfter(latest.time)) { latest = ( eventId: e.eventId, senderId: e.senderId, time: e.originServerTs, ); } } } return latest; } /// Whether the thread under [root] has a reply we haven't read: one from /// someone else, newer than our receipt in the thread and than our /// unthreaded one (which covers everything before it). bool threadUnread(Event root, [Timeline? timeline]) { final latest = latestThreadReply(root, timeline); if (latest == null) return false; final room = root.room; if (latest.senderId == room.client.userID) return false; final inThread = ownThreadReceipt(room, root.eventId); if (inThread?.eventId == latest.eventId) return false; final unthreaded = room.receiptState.global.latestOwnReceipt; if (unthreaded?.eventId == latest.eventId) return false; for (final r in [inThread, unthreaded]) { if (r != null && !latest.time.isAfter(r.timestamp)) return false; } return true; }