atagen

macltok

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