e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 1 | use std::collections::{BTreeMap, HashMap}; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 2 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 3 | use matrix_sdk::{ |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 4 | Room, RoomState, deserialized_responses::TimelineEvent, serde_helpers::extract_thread_root, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 5 | }; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 6 | use ruma::{ |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 7 | OwnedEventId, UserId, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 8 | events::receipt::{ReceiptThread, ReceiptType}, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 9 | }; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 10 | use serde_json::Value; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 11 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 12 | use crate::ThreadUnread; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 13 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 14 | struct Reply { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 15 | event_id: Option<OwnedEventId>, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 16 | own: bool, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 17 | counts: bool, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 18 | notify: bool, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 19 | highlight: bool, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 20 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 21 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 22 | impl Reply { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 23 | fn of(event: &TimelineEvent, own_user_id: &UserId) -> Self { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 24 | let own = event.sender().as_deref() == Some(own_user_id); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 25 | let counts = !own && counts_as_unread(event); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 26 | let actions = event.push_actions().unwrap_or_default(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 27 | Self { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 28 | event_id: event.event_id(), |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 29 | own, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 30 | counts, |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 31 | notify: counts && actions.iter().any(|action| action.should_notify()), |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 32 | highlight: counts && actions.iter().any(|action| action.is_highlight()), |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 33 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 34 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 35 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 36 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 37 | /// the sdk leaves thread events out of the room's counts once threads are on and keeps none |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 38 | /// of its own for them. so these are worked out the same way over the events it has in |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 39 | /// memory, newest first until a receipt for that thread (msc3771), an unthreaded receipt or |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 40 | /// the user's own reply. a receipt older than memory counts everything in memory, as the |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 41 | /// sdk's does |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 42 | pub(crate) async fn thread_unreads(room: &Room) -> BTreeMap<OwnedEventId, ThreadUnread> { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 43 | if room.state() != RoomState::Joined { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 44 | return BTreeMap::new(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 45 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 46 | let own_user_id = room.own_user_id(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 47 | let unthreaded = own_receipts(room, ReceiptThread::Unthreaded).await; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 48 | let Ok((cache, _drop_handles)) = room.event_cache().await else { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 49 | return BTreeMap::new(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 50 | }; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 51 | let mut threads: HashMap<OwnedEventId, Vec<Reply>> = HashMap::new(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 52 | let scanned = cache |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 53 | .rfind_map_event_in_memory_by(|event| { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 54 | let event_id = event.event_id(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 55 | if event_id.is_some_and(|event_id| unthreaded.contains(&event_id)) { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 56 | return Some(()); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 57 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 58 | if let Some(root) = extract_thread_root(event.raw()) { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 59 | threads |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 60 | .entry(root) |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 61 | .or_default() |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 62 | .push(Reply::of(event, own_user_id)); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 63 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 64 | None |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 65 | }) |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 66 | .await; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 67 | if let Err(error) = scanned { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 68 | tracing::debug!(room_id = %room.room_id(), %error, "reading events for thread unreads failed"); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 69 | return BTreeMap::new(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 70 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 71 | let mut unreads = BTreeMap::new(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 72 | for (root, replies) in threads { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 73 | let read = own_receipts(room, ReceiptThread::Thread(root.clone())).await; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 74 | let mut unread = ThreadUnread::default(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 75 | for reply in replies { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 76 | if reply.own |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 77 | || reply |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 78 | .event_id |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 79 | .as_ref() |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 80 | .is_some_and(|event_id| read.contains(event_id)) |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 81 | { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 82 | break; |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 83 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 84 | unread.messages += u64::from(reply.counts); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 85 | unread.notifications += u64::from(reply.notify); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 86 | unread.mentions += u64::from(reply.highlight); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 87 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 88 | if unread != ThreadUnread::default() { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 89 | unreads.insert(root, unread); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 90 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 91 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 92 | unreads |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 93 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 94 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 95 | async fn own_receipts(room: &Room, thread: ReceiptThread) -> Vec<OwnedEventId> { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 96 | let mut found = Vec::new(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 97 | for receipt_type in [ReceiptType::Read, ReceiptType::ReadPrivate] { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 98 | match room |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 99 | .load_user_receipt(receipt_type, thread.clone(), room.own_user_id()) |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 100 | .await |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 101 | { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 102 | Ok(Some((event_id, _))) => found.push(event_id), |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 103 | Ok(None) => {} |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 104 | Err(error) => { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 105 | tracing::debug!(room_id = %room.room_id(), %error, "reading a receipt failed"); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 106 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 107 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 108 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 109 | found |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 110 | } |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 111 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 112 | /// the sdk's rule for its room counts, which is private to it |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 113 | fn counts_as_unread(event: &TimelineEvent) -> bool { |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 114 | let raw = event.raw(); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 115 | let redacted = raw |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 116 | .get_field::<Value>("unsigned") |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 117 | .ok() |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 118 | .flatten() |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 119 | .is_some_and(|unsigned| unsigned.get("redacted_because").is_some()); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 120 | let message_like = matches!( |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 121 | raw.get_field::<String>("type").ok().flatten().as_deref(), |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 122 | Some( |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 123 | "m.room.message" |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 124 | | "m.room.encrypted" |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 125 | | "m.sticker" |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 126 | | "m.poll.start" |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 127 | | "org.matrix.msc3381.poll.start" |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 128 | | "m.poll.end" |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 129 | | "org.matrix.msc3381.poll.end" |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 130 | ) |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 131 | ); |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 132 | message_like && !redacted |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 133 | } |