atagen

macltok

public
Star0
Watch0
21b9227729a49cccc12924f2ca63c0f1890c6b89
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/normalise/thread.rs

21b9227729a4atagen2026-10-25 11:03 UTC1
use std::collections::{BTreeMap, HashSet};
e5c6427adb35atagen2026-10-25 10:23 UTC2
e5c6427adb35atagen2026-10-25 10:23 UTC3
use matrix_sdk::{
21b9227729a4atagen2026-10-25 11:03 UTC4
    Room, RoomState, cross_process_lock::MappedCrossProcessLockState,
21b9227729a4atagen2026-10-25 11:03 UTC5
    deserialized_responses::TimelineEvent,
e5c6427adb35atagen2026-10-25 10:23 UTC6
};
21b9227729a4atagen2026-10-25 11:03 UTC7
use matrix_sdk_base::event_cache::store::EventCacheStoreLockGuard;
e5c6427adb35atagen2026-10-25 10:23 UTC8
use ruma::{
21b9227729a4atagen2026-10-25 11:03 UTC9
    OwnedEventId,
21b9227729a4atagen2026-10-25 11:03 UTC10
    events::{
21b9227729a4atagen2026-10-25 11:03 UTC11
        receipt::{ReceiptThread, ReceiptType},
21b9227729a4atagen2026-10-25 11:03 UTC12
        relation::RelationType,
21b9227729a4atagen2026-10-25 11:03 UTC13
    },
e5c6427adb35atagen2026-10-25 10:23 UTC14
};
21b9227729a4atagen2026-10-25 11:03 UTC15
use serde_json::{Value, json};
e5c6427adb35atagen2026-10-25 10:23 UTC16
e5c6427adb35atagen2026-10-25 10:23 UTC17
use crate::ThreadUnread;
e5c6427adb35atagen2026-10-25 10:23 UTC18
21b9227729a4atagen2026-10-25 11:03 UTC19
fn key(room: &Room) -> Vec<u8> {
21b9227729a4atagen2026-10-25 11:03 UTC20
    format!("macltok.thread_unreads.{}", room.room_id()).into_bytes()
e5c6427adb35atagen2026-10-25 10:23 UTC21
}
e5c6427adb35atagen2026-10-25 10:23 UTC22
21b9227729a4atagen2026-10-25 11:03 UTC23
/// what the last recount left, so a room list never touches the event cache
21b9227729a4atagen2026-10-25 11:03 UTC24
pub(crate) async fn stored_thread_unreads(room: &Room) -> BTreeMap<OwnedEventId, ThreadUnread> {
21b9227729a4atagen2026-10-25 11:03 UTC25
    if room.state() != RoomState::Joined {
21b9227729a4atagen2026-10-25 11:03 UTC26
        return BTreeMap::new();
21b9227729a4atagen2026-10-25 11:03 UTC27
    }
21b9227729a4atagen2026-10-25 11:03 UTC28
    match room.client().state_store().get_custom_value(&key(room)).await {
21b9227729a4atagen2026-10-25 11:03 UTC29
        Ok(Some(bytes)) => decode(&bytes),
21b9227729a4atagen2026-10-25 11:03 UTC30
        Ok(None) => BTreeMap::new(),
21b9227729a4atagen2026-10-25 11:03 UTC31
        Err(error) => {
21b9227729a4atagen2026-10-25 11:03 UTC32
            tracing::debug!(room_id = %room.room_id(), %error, "reading thread unreads failed");
21b9227729a4atagen2026-10-25 11:03 UTC33
            BTreeMap::new()
e5c6427adb35atagen2026-10-25 10:23 UTC34
        }
e5c6427adb35atagen2026-10-25 10:23 UTC35
    }
e5c6427adb35atagen2026-10-25 10:23 UTC36
}
e5c6427adb35atagen2026-10-25 10:23 UTC37
e5c6427adb35atagen2026-10-25 10:23 UTC38
/// the sdk leaves thread events out of the room's counts once threads are on and keeps none
21b9227729a4atagen2026-10-25 11:03 UTC39
/// of its own for them. this counts `touched` and every thread that still had something
21b9227729a4atagen2026-10-25 11:03 UTC40
/// unread from the event cache store, which holds every reply it ever took in whatever is
21b9227729a4atagen2026-10-25 11:03 UTC41
/// in memory, against a receipt for that thread (msc3771), an unthreaded one or the user's
21b9227729a4atagen2026-10-25 11:03 UTC42
/// own reply. `None` when nothing moved
21b9227729a4atagen2026-10-25 11:03 UTC43
pub(crate) async fn recount_thread_unreads(
21b9227729a4atagen2026-10-25 11:03 UTC44
    room: &Room,
21b9227729a4atagen2026-10-25 11:03 UTC45
    touched: HashSet<OwnedEventId>,
21b9227729a4atagen2026-10-25 11:03 UTC46
) -> Option<BTreeMap<OwnedEventId, ThreadUnread>> {
21b9227729a4atagen2026-10-25 11:03 UTC47
    let known = stored_thread_unreads(room).await;
21b9227729a4atagen2026-10-25 11:03 UTC48
    let mut roots = touched;
21b9227729a4atagen2026-10-25 11:03 UTC49
    roots.extend(known.keys().cloned());
21b9227729a4atagen2026-10-25 11:03 UTC50
    if roots.is_empty() || room.state() != RoomState::Joined {
21b9227729a4atagen2026-10-25 11:03 UTC51
        return None;
e5c6427adb35atagen2026-10-25 10:23 UTC52
    }
21b9227729a4atagen2026-10-25 11:03 UTC53
    let store = match room.client().event_cache_store().lock().await {
21b9227729a4atagen2026-10-25 11:03 UTC54
        Ok(MappedCrossProcessLockState::Clean(store) | MappedCrossProcessLockState::Dirty(store)) => {
21b9227729a4atagen2026-10-25 11:03 UTC55
            store
21b9227729a4atagen2026-10-25 11:03 UTC56
        }
21b9227729a4atagen2026-10-25 11:03 UTC57
        Err(error) => {
21b9227729a4atagen2026-10-25 11:03 UTC58
            tracing::debug!(room_id = %room.room_id(), %error, "locking the event cache store failed");
21b9227729a4atagen2026-10-25 11:03 UTC59
            return None;
21b9227729a4atagen2026-10-25 11:03 UTC60
        }
e5c6427adb35atagen2026-10-25 10:23 UTC61
    };
21b9227729a4atagen2026-10-25 11:03 UTC62
    let own_user_id = room.own_user_id();
21b9227729a4atagen2026-10-25 11:03 UTC63
    let unthreaded = read_through(room, &store, ReceiptThread::Unthreaded).await;
21b9227729a4atagen2026-10-25 11:03 UTC64
    let mut counted = BTreeMap::new();
21b9227729a4atagen2026-10-25 11:03 UTC65
    for root in roots {
21b9227729a4atagen2026-10-25 11:03 UTC66
        let replies = match store
21b9227729a4atagen2026-10-25 11:03 UTC67
            .find_event_relations(room.room_id(), &root, Some(&[RelationType::Thread][..]))
21b9227729a4atagen2026-10-25 11:03 UTC68
            .await
21b9227729a4atagen2026-10-25 11:03 UTC69
        {
21b9227729a4atagen2026-10-25 11:03 UTC70
            Ok(replies) => replies,
21b9227729a4atagen2026-10-25 11:03 UTC71
            Err(error) => {
21b9227729a4atagen2026-10-25 11:03 UTC72
                tracing::debug!(room_id = %room.room_id(), %root, %error, "reading a thread failed");
21b9227729a4atagen2026-10-25 11:03 UTC73
                if let Some(unread) = known.get(&root) {
21b9227729a4atagen2026-10-25 11:03 UTC74
                    counted.insert(root, *unread);
21b9227729a4atagen2026-10-25 11:03 UTC75
                }
21b9227729a4atagen2026-10-25 11:03 UTC76
                continue;
e5c6427adb35atagen2026-10-25 10:23 UTC77
            }
21b9227729a4atagen2026-10-25 11:03 UTC78
        };
21b9227729a4atagen2026-10-25 11:03 UTC79
        let mut read = unthreaded.max(read_through(room, &store, ReceiptThread::Thread(root.clone())).await);
21b9227729a4atagen2026-10-25 11:03 UTC80
        for (reply, _) in &replies {
21b9227729a4atagen2026-10-25 11:03 UTC81
            if reply.sender().as_deref() == Some(own_user_id) {
21b9227729a4atagen2026-10-25 11:03 UTC82
                read = read.max(timestamp_ms(reply));
e5c6427adb35atagen2026-10-25 10:23 UTC83
            }
21b9227729a4atagen2026-10-25 11:03 UTC84
        }
e5c6427adb35atagen2026-10-25 10:23 UTC85
        let mut unread = ThreadUnread::default();
21b9227729a4atagen2026-10-25 11:03 UTC86
        for (reply, _) in &replies {
21b9227729a4atagen2026-10-25 11:03 UTC87
            let Some(at) = timestamp_ms(reply) else {
21b9227729a4atagen2026-10-25 11:03 UTC88
                continue;
21b9227729a4atagen2026-10-25 11:03 UTC89
            };
21b9227729a4atagen2026-10-25 11:03 UTC90
            if read.is_some_and(|read| at <= read)
21b9227729a4atagen2026-10-25 11:03 UTC91
                || reply.sender().as_deref() == Some(own_user_id)
21b9227729a4atagen2026-10-25 11:03 UTC92
                || !counts_as_unread(reply)
e5c6427adb35atagen2026-10-25 10:23 UTC93
            {
21b9227729a4atagen2026-10-25 11:03 UTC94
                continue;
e5c6427adb35atagen2026-10-25 10:23 UTC95
            }
21b9227729a4atagen2026-10-25 11:03 UTC96
            let actions = reply.push_actions().unwrap_or_default();
21b9227729a4atagen2026-10-25 11:03 UTC97
            unread.messages += 1;
21b9227729a4atagen2026-10-25 11:03 UTC98
            unread.notifications += u64::from(actions.iter().any(|action| action.should_notify()));
21b9227729a4atagen2026-10-25 11:03 UTC99
            unread.mentions += u64::from(actions.iter().any(|action| action.is_highlight()));
e5c6427adb35atagen2026-10-25 10:23 UTC100
        }
e5c6427adb35atagen2026-10-25 10:23 UTC101
        if unread != ThreadUnread::default() {
21b9227729a4atagen2026-10-25 11:03 UTC102
            counted.insert(root, unread);
e5c6427adb35atagen2026-10-25 10:23 UTC103
        }
e5c6427adb35atagen2026-10-25 10:23 UTC104
    }
21b9227729a4atagen2026-10-25 11:03 UTC105
    if counted == known {
21b9227729a4atagen2026-10-25 11:03 UTC106
        return None;
21b9227729a4atagen2026-10-25 11:03 UTC107
    }
21b9227729a4atagen2026-10-25 11:03 UTC108
    let stored = room
21b9227729a4atagen2026-10-25 11:03 UTC109
        .client()
21b9227729a4atagen2026-10-25 11:03 UTC110
        .state_store()
21b9227729a4atagen2026-10-25 11:03 UTC111
        .set_custom_value(&key(room), encode(&counted))
21b9227729a4atagen2026-10-25 11:03 UTC112
        .await;
21b9227729a4atagen2026-10-25 11:03 UTC113
    if let Err(error) = stored {
21b9227729a4atagen2026-10-25 11:03 UTC114
        tracing::debug!(room_id = %room.room_id(), %error, "saving thread unreads failed");
21b9227729a4atagen2026-10-25 11:03 UTC115
    }
21b9227729a4atagen2026-10-25 11:03 UTC116
    Some(counted)
e5c6427adb35atagen2026-10-25 10:23 UTC117
}
e5c6427adb35atagen2026-10-25 10:23 UTC118
21b9227729a4atagen2026-10-25 11:03 UTC119
/// receipts sit on events, and stream order between a thread and the rest of the room is
21b9227729a4atagen2026-10-25 11:03 UTC120
/// gone once it's in the store, so the event's timestamp stands for its place. a receipt
21b9227729a4atagen2026-10-25 11:03 UTC121
/// on an event the store never had falls back to when it was sent
21b9227729a4atagen2026-10-25 11:03 UTC122
async fn read_through(
21b9227729a4atagen2026-10-25 11:03 UTC123
    room: &Room,
21b9227729a4atagen2026-10-25 11:03 UTC124
    store: &EventCacheStoreLockGuard,
21b9227729a4atagen2026-10-25 11:03 UTC125
    thread: ReceiptThread,
21b9227729a4atagen2026-10-25 11:03 UTC126
) -> Option<u64> {
21b9227729a4atagen2026-10-25 11:03 UTC127
    let mut latest = None;
e5c6427adb35atagen2026-10-25 10:23 UTC128
    for receipt_type in [ReceiptType::Read, ReceiptType::ReadPrivate] {
21b9227729a4atagen2026-10-25 11:03 UTC129
        let (event_id, receipt) = match room
e5c6427adb35atagen2026-10-25 10:23 UTC130
            .load_user_receipt(receipt_type, thread.clone(), room.own_user_id())
e5c6427adb35atagen2026-10-25 10:23 UTC131
            .await
e5c6427adb35atagen2026-10-25 10:23 UTC132
        {
21b9227729a4atagen2026-10-25 11:03 UTC133
            Ok(Some(found)) => found,
21b9227729a4atagen2026-10-25 11:03 UTC134
            Ok(None) => continue,
e5c6427adb35atagen2026-10-25 10:23 UTC135
            Err(error) => {
e5c6427adb35atagen2026-10-25 10:23 UTC136
                tracing::debug!(room_id = %room.room_id(), %error, "reading a receipt failed");
21b9227729a4atagen2026-10-25 11:03 UTC137
                continue;
e5c6427adb35atagen2026-10-25 10:23 UTC138
            }
21b9227729a4atagen2026-10-25 11:03 UTC139
        };
21b9227729a4atagen2026-10-25 11:03 UTC140
        let at = match store.find_event(room.room_id(), &event_id).await {
21b9227729a4atagen2026-10-25 11:03 UTC141
            Ok(Some(event)) => timestamp_ms(&event),
21b9227729a4atagen2026-10-25 11:03 UTC142
            _ => None,
e5c6427adb35atagen2026-10-25 10:23 UTC143
        }
21b9227729a4atagen2026-10-25 11:03 UTC144
        .or_else(|| receipt.ts.map(|ts| u64::from(ts.get())));
21b9227729a4atagen2026-10-25 11:03 UTC145
        latest = latest.max(at);
e5c6427adb35atagen2026-10-25 10:23 UTC146
    }
21b9227729a4atagen2026-10-25 11:03 UTC147
    latest
e5c6427adb35atagen2026-10-25 10:23 UTC148
}
e5c6427adb35atagen2026-10-25 10:23 UTC149
21b9227729a4atagen2026-10-25 11:03 UTC150
fn timestamp_ms(event: &TimelineEvent) -> Option<u64> {
21b9227729a4atagen2026-10-25 11:03 UTC151
    event.timestamp().map(|ts| u64::from(ts.get()))
21b9227729a4atagen2026-10-25 11:03 UTC152
}
21b9227729a4atagen2026-10-25 11:03 UTC153
21b9227729a4atagen2026-10-25 11:03 UTC154
/// the sdk's `marks_as_unread` for its room counts, which is private to it
e5c6427adb35atagen2026-10-25 10:23 UTC155
fn counts_as_unread(event: &TimelineEvent) -> bool {
e5c6427adb35atagen2026-10-25 10:23 UTC156
    let raw = event.raw();
e5c6427adb35atagen2026-10-25 10:23 UTC157
    let redacted = raw
e5c6427adb35atagen2026-10-25 10:23 UTC158
        .get_field::<Value>("unsigned")
e5c6427adb35atagen2026-10-25 10:23 UTC159
        .ok()
e5c6427adb35atagen2026-10-25 10:23 UTC160
        .flatten()
e5c6427adb35atagen2026-10-25 10:23 UTC161
        .is_some_and(|unsigned| unsigned.get("redacted_because").is_some());
21b9227729a4atagen2026-10-25 11:03 UTC162
    let edit = raw
21b9227729a4atagen2026-10-25 11:03 UTC163
        .get_field::<Value>("content")
21b9227729a4atagen2026-10-25 11:03 UTC164
        .ok()
21b9227729a4atagen2026-10-25 11:03 UTC165
        .flatten()
21b9227729a4atagen2026-10-25 11:03 UTC166
        .and_then(|content| {
21b9227729a4atagen2026-10-25 11:03 UTC167
            content
21b9227729a4atagen2026-10-25 11:03 UTC168
                .pointer("/m.relates_to/rel_type")
21b9227729a4atagen2026-10-25 11:03 UTC169
                .and_then(Value::as_str)
21b9227729a4atagen2026-10-25 11:03 UTC170
                .map(|rel_type| rel_type == "m.replace")
21b9227729a4atagen2026-10-25 11:03 UTC171
        })
21b9227729a4atagen2026-10-25 11:03 UTC172
        .unwrap_or(false);
e5c6427adb35atagen2026-10-25 10:23 UTC173
    let message_like = matches!(
e5c6427adb35atagen2026-10-25 10:23 UTC174
        raw.get_field::<String>("type").ok().flatten().as_deref(),
e5c6427adb35atagen2026-10-25 10:23 UTC175
        Some(
21b9227729a4atagen2026-10-25 11:03 UTC176
            "m.message"
21b9227729a4atagen2026-10-25 11:03 UTC177
                | "m.room.message"
e5c6427adb35atagen2026-10-25 10:23 UTC178
                | "m.room.encrypted"
e5c6427adb35atagen2026-10-25 10:23 UTC179
                | "m.sticker"
e5c6427adb35atagen2026-10-25 10:23 UTC180
                | "m.poll.start"
e5c6427adb35atagen2026-10-25 10:23 UTC181
                | "org.matrix.msc3381.poll.start"
e5c6427adb35atagen2026-10-25 10:23 UTC182
                | "m.poll.end"
e5c6427adb35atagen2026-10-25 10:23 UTC183
                | "org.matrix.msc3381.poll.end"
e5c6427adb35atagen2026-10-25 10:23 UTC184
        )
e5c6427adb35atagen2026-10-25 10:23 UTC185
    );
21b9227729a4atagen2026-10-25 11:03 UTC186
    message_like && !edit && !redacted
21b9227729a4atagen2026-10-25 11:03 UTC187
}
21b9227729a4atagen2026-10-25 11:03 UTC188
21b9227729a4atagen2026-10-25 11:03 UTC189
fn encode(unreads: &BTreeMap<OwnedEventId, ThreadUnread>) -> Vec<u8> {
21b9227729a4atagen2026-10-25 11:03 UTC190
    let value: serde_json::Map<String, Value> = unreads
21b9227729a4atagen2026-10-25 11:03 UTC191
        .iter()
21b9227729a4atagen2026-10-25 11:03 UTC192
        .map(|(root, unread)| {
21b9227729a4atagen2026-10-25 11:03 UTC193
            (
21b9227729a4atagen2026-10-25 11:03 UTC194
                root.to_string(),
21b9227729a4atagen2026-10-25 11:03 UTC195
                json!([unread.messages, unread.mentions, unread.notifications]),
21b9227729a4atagen2026-10-25 11:03 UTC196
            )
21b9227729a4atagen2026-10-25 11:03 UTC197
        })
21b9227729a4atagen2026-10-25 11:03 UTC198
        .collect();
21b9227729a4atagen2026-10-25 11:03 UTC199
    serde_json::to_vec(&value).unwrap_or_default()
21b9227729a4atagen2026-10-25 11:03 UTC200
}
21b9227729a4atagen2026-10-25 11:03 UTC201
21b9227729a4atagen2026-10-25 11:03 UTC202
fn decode(bytes: &[u8]) -> BTreeMap<OwnedEventId, ThreadUnread> {
21b9227729a4atagen2026-10-25 11:03 UTC203
    let Ok(value) = serde_json::from_slice::<BTreeMap<OwnedEventId, [u64; 3]>>(bytes) else {
21b9227729a4atagen2026-10-25 11:03 UTC204
        return BTreeMap::new();
21b9227729a4atagen2026-10-25 11:03 UTC205
    };
21b9227729a4atagen2026-10-25 11:03 UTC206
    value
21b9227729a4atagen2026-10-25 11:03 UTC207
        .into_iter()
21b9227729a4atagen2026-10-25 11:03 UTC208
        .map(|(root, [messages, mentions, notifications])| {
21b9227729a4atagen2026-10-25 11:03 UTC209
            (
21b9227729a4atagen2026-10-25 11:03 UTC210
                root,
21b9227729a4atagen2026-10-25 11:03 UTC211
                ThreadUnread {
21b9227729a4atagen2026-10-25 11:03 UTC212
                    messages,
21b9227729a4atagen2026-10-25 11:03 UTC213
                    mentions,
21b9227729a4atagen2026-10-25 11:03 UTC214
                    notifications,
21b9227729a4atagen2026-10-25 11:03 UTC215
                },
21b9227729a4atagen2026-10-25 11:03 UTC216
            )
21b9227729a4atagen2026-10-25 11:03 UTC217
        })
21b9227729a4atagen2026-10-25 11:03 UTC218
        .collect()
e5c6427adb35atagen2026-10-25 10:23 UTC219
}