atagen

macltok

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

Blame: src/engine/watchers.rs

411bc1c78b65atagen2026-10-22 07:39 UTC1
use std::{
21b9227729a4atagen2026-10-25 11:03 UTC2
    collections::{HashMap, HashSet, VecDeque},
411bc1c78b65atagen2026-10-22 07:39 UTC3
    sync::Arc,
411bc1c78b65atagen2026-10-22 07:39 UTC4
    time::Duration,
411bc1c78b65atagen2026-10-22 07:39 UTC5
};
31ce2453c82fatagen2026-09-24 13:07 UTC6
31ce2453c82fatagen2026-09-24 13:07 UTC7
use futures_util::StreamExt;
31ce2453c82fatagen2026-09-24 13:07 UTC8
use matrix_sdk::{
e5c6427adb35atagen2026-10-25 10:23 UTC9
    Client, Room,
21b9227729a4atagen2026-10-25 11:03 UTC10
    event_handler::EventHandlerHandle,
31ce2453c82fatagen2026-09-24 13:07 UTC11
    ruma::events::{AnyMessageLikeEventContent, room::message::MessageType},
0bfa3f46f0beatagen2026-09-26 09:12 UTC12
    send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendQueueUpdate},
60454002af5fatagen2026-10-25 11:13 UTC13
    serde_helpers::extract_thread_root,
31ce2453c82fatagen2026-09-24 13:07 UTC14
};
17e042fbc331atagen2026-10-08 12:39 UTC15
use matrix_sdk_base::RoomInfoNotableUpdateReasons;
31ce2453c82fatagen2026-09-24 13:07 UTC16
use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};
17e042fbc331atagen2026-10-08 12:39 UTC17
use ruma::{
e5c6427adb35atagen2026-10-25 10:23 UTC18
    OwnedEventId, OwnedRoomId, OwnedTransactionId,
e5c6427adb35atagen2026-10-25 10:23 UTC19
    events::{
e5c6427adb35atagen2026-10-25 10:23 UTC20
        AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent,
21b9227729a4atagen2026-10-25 11:03 UTC21
        AnySyncTimelineEvent, receipt::SyncReceiptEvent,
e5c6427adb35atagen2026-10-25 10:23 UTC22
    },
17e042fbc331atagen2026-10-08 12:39 UTC23
    serde::Raw,
17e042fbc331atagen2026-10-08 12:39 UTC24
    time::Instant,
17e042fbc331atagen2026-10-08 12:39 UTC25
};
21b9227729a4atagen2026-10-25 11:03 UTC26
use tokio::sync::{
21b9227729a4atagen2026-10-25 11:03 UTC27
    Notify,
21b9227729a4atagen2026-10-25 11:03 UTC28
    broadcast::error::{RecvError, TryRecvError},
21b9227729a4atagen2026-10-25 11:03 UTC29
    mpsc, watch,
21b9227729a4atagen2026-10-25 11:03 UTC30
};
31ce2453c82fatagen2026-09-24 13:07 UTC31
f12377bce9b9atagen2026-09-26 12:32 UTC32
use super::executor::TaskSet;
31ce2453c82fatagen2026-09-24 13:07 UTC33
use crate::{
21b9227729a4atagen2026-10-25 11:03 UTC34
    ConnectionState, Event, UploadOutcome,
17e042fbc331atagen2026-10-08 12:39 UTC35
    encryption::encryption_state,
17e042fbc331atagen2026-10-08 12:39 UTC36
    error::classify_sdk_error,
21b9227729a4atagen2026-10-25 11:03 UTC37
    normalise::{ENABLED_ROOMS, ROOM_PACKS, USER_PACK, recount_thread_unreads},
9bd34e25bb86atagen2026-10-24 07:23 UTC38
    space_order,
f12377bce9b9atagen2026-09-26 12:32 UTC39
    transport::EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC40
};
31ce2453c82fatagen2026-09-24 13:07 UTC41
31ce2453c82fatagen2026-09-24 13:07 UTC42
const SYNC_RESTART_MIN: Duration = Duration::from_secs(1);
31ce2453c82fatagen2026-09-24 13:07 UTC43
const SYNC_RESTART_MAX: Duration = Duration::from_secs(60);
31ce2453c82fatagen2026-09-24 13:07 UTC44
const SEND_QUEUE_RETRY: Duration = Duration::from_secs(5);
5a52012c9b1datagen2026-09-24 23:45 UTC45
const SYNC_STABLE: Duration = Duration::from_secs(30);
5a52012c9b1datagen2026-09-24 23:45 UTC46
5a52012c9b1datagen2026-09-24 23:45 UTC47
pub(super) async fn room_subscriptions(
5a52012c9b1datagen2026-09-24 23:45 UTC48
    sync: Arc<SyncService>,
5a52012c9b1datagen2026-09-24 23:45 UTC49
    mut rooms: watch::Receiver<Vec<ruma::OwnedRoomId>>,
5a52012c9b1datagen2026-09-24 23:45 UTC50
) {
5a52012c9b1datagen2026-09-24 23:45 UTC51
    while rooms.changed().await.is_ok() {
5a52012c9b1datagen2026-09-24 23:45 UTC52
        let room_ids = rooms.borrow_and_update().clone();
5a52012c9b1datagen2026-09-24 23:45 UTC53
        let room_ids = room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>();
5a52012c9b1datagen2026-09-24 23:45 UTC54
        sync.room_list_service().subscribe_to_rooms(&room_ids).await;
5a52012c9b1datagen2026-09-24 23:45 UTC55
    }
5a52012c9b1datagen2026-09-24 23:45 UTC56
}
31ce2453c82fatagen2026-09-24 13:07 UTC57
31ce2453c82fatagen2026-09-24 13:07 UTC58
/// `states` must be taken before sync starts, a late subscriber never hears the transition it missed
31ce2453c82fatagen2026-09-24 13:07 UTC59
pub(super) async fn connection(
31ce2453c82fatagen2026-09-24 13:07 UTC60
    mut states: impl futures_util::Stream<Item = SyncState> + Unpin,
31ce2453c82fatagen2026-09-24 13:07 UTC61
    sync: Arc<SyncService>,
31ce2453c82fatagen2026-09-24 13:07 UTC62
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC63
    events: EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC64
    connection: watch::Sender<ConnectionState>,
31ce2453c82fatagen2026-09-24 13:07 UTC65
) {
31ce2453c82fatagen2026-09-24 13:07 UTC66
    let mut backoff = SYNC_RESTART_MIN;
5a52012c9b1datagen2026-09-24 23:45 UTC67
    let mut online_since: Option<Instant> = None;
31ce2453c82fatagen2026-09-24 13:07 UTC68
    while let Some(state) = states.next().await {
5a52012c9b1datagen2026-09-24 23:45 UTC69
        // only a run that lasted counts as recovered, or a server that answers /versions
5a52012c9b1datagen2026-09-24 23:45 UTC70
        // but refuses sync would have us bounce between offline and online forever
5a52012c9b1datagen2026-09-24 23:45 UTC71
        let stable = online_since
5a52012c9b1datagen2026-09-24 23:45 UTC72
            .take()
5a52012c9b1datagen2026-09-24 23:45 UTC73
            .map(|since| since.elapsed() >= SYNC_STABLE);
5a52012c9b1datagen2026-09-24 23:45 UTC74
        if stable == Some(true) {
5a52012c9b1datagen2026-09-24 23:45 UTC75
            backoff = SYNC_RESTART_MIN;
5a52012c9b1datagen2026-09-24 23:45 UTC76
        }
31ce2453c82fatagen2026-09-24 13:07 UTC77
        let (state, restart) = match state {
5a52012c9b1datagen2026-09-24 23:45 UTC78
            // only ever us stopping it for a restart, nothing a client needs to hear
5a52012c9b1datagen2026-09-24 23:45 UTC79
            SyncState::Idle => continue,
31ce2453c82fatagen2026-09-24 13:07 UTC80
            SyncState::Running => {
5a52012c9b1datagen2026-09-24 23:45 UTC81
                online_since = Some(Instant::now());
31ce2453c82fatagen2026-09-24 13:07 UTC82
                // the sdk parks a room queue on any send error and never unparks it
31ce2453c82fatagen2026-09-24 13:07 UTC83
                client.send_queue().set_enabled(true).await;
31ce2453c82fatagen2026-09-24 13:07 UTC84
                (ConnectionState::Online, false)
31ce2453c82fatagen2026-09-24 13:07 UTC85
            }
5a52012c9b1datagen2026-09-24 23:45 UTC86
            SyncState::Offline => (ConnectionState::Offline, stable == Some(false)),
31ce2453c82fatagen2026-09-24 13:07 UTC87
            SyncState::Terminated => (ConnectionState::Offline, true),
31ce2453c82fatagen2026-09-24 13:07 UTC88
            SyncState::Error(error) => {
31ce2453c82fatagen2026-09-24 13:07 UTC89
                tracing::warn!(%error, "sync service reported an error");
31ce2453c82fatagen2026-09-24 13:07 UTC90
                (ConnectionState::Offline, true)
31ce2453c82fatagen2026-09-24 13:07 UTC91
            }
31ce2453c82fatagen2026-09-24 13:07 UTC92
        };
31ce2453c82fatagen2026-09-24 13:07 UTC93
        if *connection.borrow() != state {
31ce2453c82fatagen2026-09-24 13:07 UTC94
            connection.send_replace(state);
31ce2453c82fatagen2026-09-24 13:07 UTC95
            if events.send(Event::ConnectionChanged(state)).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC96
                return;
31ce2453c82fatagen2026-09-24 13:07 UTC97
            }
31ce2453c82fatagen2026-09-24 13:07 UTC98
        }
31ce2453c82fatagen2026-09-24 13:07 UTC99
        if restart {
5a52012c9b1datagen2026-09-24 23:45 UTC100
            sync.stop().await;
1fe480c16a3catagen2026-09-26 08:44 UTC101
            matrix_sdk::sleep::sleep(backoff).await;
31ce2453c82fatagen2026-09-24 13:07 UTC102
            backoff = (backoff * 2).min(SYNC_RESTART_MAX);
31ce2453c82fatagen2026-09-24 13:07 UTC103
            sync.start().await;
31ce2453c82fatagen2026-09-24 13:07 UTC104
        }
31ce2453c82fatagen2026-09-24 13:07 UTC105
    }
31ce2453c82fatagen2026-09-24 13:07 UTC106
}
31ce2453c82fatagen2026-09-24 13:07 UTC107
31ce2453c82fatagen2026-09-24 13:07 UTC108
/// a recoverable failure while we're online is a hiccup, so give the room another go
31ce2453c82fatagen2026-09-24 13:07 UTC109
pub(super) async fn send_queue_errors(
31ce2453c82fatagen2026-09-24 13:07 UTC110
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC111
    mut errors: tokio::sync::broadcast::Receiver<matrix_sdk::send_queue::SendQueueRoomError>,
31ce2453c82fatagen2026-09-24 13:07 UTC112
    online: watch::Receiver<ConnectionState>,
31ce2453c82fatagen2026-09-24 13:07 UTC113
) {
1fe480c16a3catagen2026-09-26 08:44 UTC114
    let mut retries = TaskSet::new();
31ce2453c82fatagen2026-09-24 13:07 UTC115
    loop {
31ce2453c82fatagen2026-09-24 13:07 UTC116
        let error = tokio::select! {
31ce2453c82fatagen2026-09-24 13:07 UTC117
            error = errors.recv() => error,
31ce2453c82fatagen2026-09-24 13:07 UTC118
            Some(_) = retries.join_next(), if !retries.is_empty() => continue,
31ce2453c82fatagen2026-09-24 13:07 UTC119
        };
31ce2453c82fatagen2026-09-24 13:07 UTC120
        match error {
31ce2453c82fatagen2026-09-24 13:07 UTC121
            Ok(error) if error.is_recoverable => {
31ce2453c82fatagen2026-09-24 13:07 UTC122
                let client = client.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC123
                let online = online.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC124
                retries.spawn(async move {
1fe480c16a3catagen2026-09-26 08:44 UTC125
                    matrix_sdk::sleep::sleep(SEND_QUEUE_RETRY).await;
31ce2453c82fatagen2026-09-24 13:07 UTC126
                    if *online.borrow() == ConnectionState::Online
31ce2453c82fatagen2026-09-24 13:07 UTC127
                        && let Some(room) = client.get_room(&error.room_id)
31ce2453c82fatagen2026-09-24 13:07 UTC128
                    {
31ce2453c82fatagen2026-09-24 13:07 UTC129
                        room.send_queue().set_enabled(true);
31ce2453c82fatagen2026-09-24 13:07 UTC130
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC131
                });
31ce2453c82fatagen2026-09-24 13:07 UTC132
            }
31ce2453c82fatagen2026-09-24 13:07 UTC133
            Ok(_) | Err(RecvError::Lagged(_)) => {}
31ce2453c82fatagen2026-09-24 13:07 UTC134
            Err(RecvError::Closed) => return,
31ce2453c82fatagen2026-09-24 13:07 UTC135
        }
31ce2453c82fatagen2026-09-24 13:07 UTC136
    }
31ce2453c82fatagen2026-09-24 13:07 UTC137
}
31ce2453c82fatagen2026-09-24 13:07 UTC138
7c26fad34822atagen2026-09-26 09:41 UTC139
/// the one place upload events leave from, so an upload is never reported after it
411bc1c78b65atagen2026-10-22 07:39 UTC140
/// ended. the client-wide channel exists before any upload does. it holds 32 updates and
411bc1c78b65atagen2026-10-22 07:39 UTC141
/// progress ticks share it, so this keeps draining while the event stream is full
0bfa3f46f0beatagen2026-09-26 09:12 UTC142
pub(super) async fn uploads(
5a52012c9b1datagen2026-09-24 23:45 UTC143
    client: Client,
0bfa3f46f0beatagen2026-09-26 09:12 UTC144
    mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,
5a52012c9b1datagen2026-09-24 23:45 UTC145
    events: EventSender,
5a52012c9b1datagen2026-09-24 23:45 UTC146
) {
0bfa3f46f0beatagen2026-09-26 09:12 UTC147
    let (_, mut rooms) = client.rooms_stream();
411bc1c78b65atagen2026-10-22 07:39 UTC148
    let mut tracker = UploadTracker::new(attachment_echoes(&client).await);
411bc1c78b65atagen2026-10-22 07:39 UTC149
    let mut pending = VecDeque::new();
5a52012c9b1datagen2026-09-24 23:45 UTC150
    loop {
411bc1c78b65atagen2026-10-22 07:39 UTC151
        let reports: Vec<Event> = tokio::select! {
411bc1c78b65atagen2026-10-22 07:39 UTC152
            biased;
0bfa3f46f0beatagen2026-09-26 09:12 UTC153
            update = updates.recv() => match update {
7c26fad34822atagen2026-09-26 09:41 UTC154
                Ok(update) => tracker.update(update).into_iter().collect(),
0bfa3f46f0beatagen2026-09-26 09:12 UTC155
                Err(RecvError::Lagged(skipped)) => {
7cf348521204atagen2026-09-26 12:28 UTC156
                    tracing::warn!(skipped, "upload tracker lagged");
7cf348521204atagen2026-09-26 12:28 UTC157
                    tracker.resnapshot(attachment_echoes(&client).await)
0bfa3f46f0beatagen2026-09-26 09:12 UTC158
                }
0bfa3f46f0beatagen2026-09-26 09:12 UTC159
                Err(RecvError::Closed) => return,
0bfa3f46f0beatagen2026-09-26 09:12 UTC160
            },
0bfa3f46f0beatagen2026-09-26 09:12 UTC161
            update = rooms.next() => {
0bfa3f46f0beatagen2026-09-26 09:12 UTC162
                if update.is_none() {
0bfa3f46f0beatagen2026-09-26 09:12 UTC163
                    return;
0bfa3f46f0beatagen2026-09-26 09:12 UTC164
                }
7c26fad34822atagen2026-09-26 09:41 UTC165
                tracker.finish_where(
7c26fad34822atagen2026-09-26 09:41 UTC166
                    |(room_id, _)| client.get_room(room_id).is_none(),
7c26fad34822atagen2026-09-26 09:41 UTC167
                    &UploadOutcome::RoomForgotten,
7c26fad34822atagen2026-09-26 09:41 UTC168
                )
31ce2453c82fatagen2026-09-24 13:07 UTC169
            }
411bc1c78b65atagen2026-10-22 07:39 UTC170
            permit = events.reserve(), if !pending.is_empty() => {
411bc1c78b65atagen2026-10-22 07:39 UTC171
                let Ok(permit) = permit else { return };
411bc1c78b65atagen2026-10-22 07:39 UTC172
                permit.send(pending.pop_front().expect("pending was checked"));
411bc1c78b65atagen2026-10-22 07:39 UTC173
                continue;
411bc1c78b65atagen2026-10-22 07:39 UTC174
            }
31ce2453c82fatagen2026-09-24 13:07 UTC175
        };
7c26fad34822atagen2026-09-26 09:41 UTC176
        for event in reports {
411bc1c78b65atagen2026-10-22 07:39 UTC177
            queue_upload_event(&mut pending, event);
411bc1c78b65atagen2026-10-22 07:39 UTC178
        }
411bc1c78b65atagen2026-10-22 07:39 UTC179
    }
411bc1c78b65atagen2026-10-22 07:39 UTC180
}
411bc1c78b65atagen2026-10-22 07:39 UTC181
411bc1c78b65atagen2026-10-22 07:39 UTC182
/// a progress tick replaces a waiting one for the same upload, unless something else about
411bc1c78b65atagen2026-10-22 07:39 UTC183
/// that upload is queued after it
411bc1c78b65atagen2026-10-22 07:39 UTC184
fn queue_upload_event(pending: &mut VecDeque<Event>, event: Event) {
411bc1c78b65atagen2026-10-22 07:39 UTC185
    if matches!(event, Event::MediaUploadProgress { .. })
411bc1c78b65atagen2026-10-22 07:39 UTC186
        && let Some(waiting) = pending
411bc1c78b65atagen2026-10-22 07:39 UTC187
            .iter_mut()
411bc1c78b65atagen2026-10-22 07:39 UTC188
            .rev()
411bc1c78b65atagen2026-10-22 07:39 UTC189
            .find(|waiting| upload_of(waiting) == upload_of(&event))
411bc1c78b65atagen2026-10-22 07:39 UTC190
        && matches!(waiting, Event::MediaUploadProgress { .. })
411bc1c78b65atagen2026-10-22 07:39 UTC191
    {
411bc1c78b65atagen2026-10-22 07:39 UTC192
        *waiting = event;
411bc1c78b65atagen2026-10-22 07:39 UTC193
        return;
411bc1c78b65atagen2026-10-22 07:39 UTC194
    }
411bc1c78b65atagen2026-10-22 07:39 UTC195
    pending.push_back(event);
411bc1c78b65atagen2026-10-22 07:39 UTC196
}
411bc1c78b65atagen2026-10-22 07:39 UTC197
411bc1c78b65atagen2026-10-22 07:39 UTC198
fn upload_of(event: &Event) -> Option<(&OwnedRoomId, &OwnedTransactionId)> {
411bc1c78b65atagen2026-10-22 07:39 UTC199
    match event {
411bc1c78b65atagen2026-10-22 07:39 UTC200
        Event::MediaUploadProgress {
411bc1c78b65atagen2026-10-22 07:39 UTC201
            room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC202
            transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC203
            ..
411bc1c78b65atagen2026-10-22 07:39 UTC204
        }
411bc1c78b65atagen2026-10-22 07:39 UTC205
        | Event::MediaUploadFailed {
411bc1c78b65atagen2026-10-22 07:39 UTC206
            room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC207
            transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC208
            ..
0bfa3f46f0beatagen2026-09-26 09:12 UTC209
        }
411bc1c78b65atagen2026-10-22 07:39 UTC210
        | Event::MediaUploadFinished {
411bc1c78b65atagen2026-10-22 07:39 UTC211
            room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC212
            transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC213
            ..
411bc1c78b65atagen2026-10-22 07:39 UTC214
        } => Some((room_id, transaction_id)),
411bc1c78b65atagen2026-10-22 07:39 UTC215
        _ => None,
0bfa3f46f0beatagen2026-09-26 09:12 UTC216
    }
0bfa3f46f0beatagen2026-09-26 09:12 UTC217
}
0bfa3f46f0beatagen2026-09-26 09:12 UTC218
0bfa3f46f0beatagen2026-09-26 09:12 UTC219
type Upload = (OwnedRoomId, OwnedTransactionId);
0bfa3f46f0beatagen2026-09-26 09:12 UTC220
411bc1c78b65atagen2026-10-22 07:39 UTC221
/// the sdk sends progress ticks from a detached task, so a tick can land after its
411bc1c78b65atagen2026-10-22 07:39 UTC222
/// upload's end. it trails by one task hop, so a short memory of ends covers it
411bc1c78b65atagen2026-10-22 07:39 UTC223
const REMEMBERED_ENDS: usize = 256;
411bc1c78b65atagen2026-10-22 07:39 UTC224
411bc1c78b65atagen2026-10-22 07:39 UTC225
/// everything reported gets exactly one `MediaUploadFinished` and nothing after it
7c26fad34822atagen2026-09-26 09:41 UTC226
struct UploadTracker {
7c26fad34822atagen2026-09-26 09:41 UTC227
    live: HashSet<Upload>,
7c26fad34822atagen2026-09-26 09:41 UTC228
    ended: HashSet<Upload>,
411bc1c78b65atagen2026-10-22 07:39 UTC229
    ended_order: VecDeque<Upload>,
0bfa3f46f0beatagen2026-09-26 09:12 UTC230
}
0bfa3f46f0beatagen2026-09-26 09:12 UTC231
7c26fad34822atagen2026-09-26 09:41 UTC232
impl UploadTracker {
411bc1c78b65atagen2026-10-22 07:39 UTC233
    fn new(live: HashSet<Upload>) -> Self {
411bc1c78b65atagen2026-10-22 07:39 UTC234
        Self {
411bc1c78b65atagen2026-10-22 07:39 UTC235
            live,
411bc1c78b65atagen2026-10-22 07:39 UTC236
            ended: HashSet::new(),
411bc1c78b65atagen2026-10-22 07:39 UTC237
            ended_order: VecDeque::new(),
411bc1c78b65atagen2026-10-22 07:39 UTC238
        }
411bc1c78b65atagen2026-10-22 07:39 UTC239
    }
411bc1c78b65atagen2026-10-22 07:39 UTC240
7c26fad34822atagen2026-09-26 09:41 UTC241
    fn update(&mut self, update: SendQueueUpdate) -> Option<Event> {
7c26fad34822atagen2026-09-26 09:41 UTC242
        let room_id = update.room_id;
7c26fad34822atagen2026-09-26 09:41 UTC243
        let (transaction_id, outcome) = match update.update {
7c26fad34822atagen2026-09-26 09:41 UTC244
            RoomSendQueueUpdate::NewLocalEvent(echo) => {
7c26fad34822atagen2026-09-26 09:41 UTC245
                let upload = (room_id, echo.transaction_id);
7c26fad34822atagen2026-09-26 09:41 UTC246
                if is_attachment_echo(&echo.content) && !self.ended.contains(&upload) {
7c26fad34822atagen2026-09-26 09:41 UTC247
                    self.live.insert(upload);
7c26fad34822atagen2026-09-26 09:41 UTC248
                }
0bfa3f46f0beatagen2026-09-26 09:12 UTC249
                return None;
0bfa3f46f0beatagen2026-09-26 09:12 UTC250
            }
411bc1c78b65atagen2026-10-22 07:39 UTC251
            RoomSendQueueUpdate::MediaUpload {
411bc1c78b65atagen2026-10-22 07:39 UTC252
                related_to,
411bc1c78b65atagen2026-10-22 07:39 UTC253
                file,
411bc1c78b65atagen2026-10-22 07:39 UTC254
                progress,
411bc1c78b65atagen2026-10-22 07:39 UTC255
                ..
411bc1c78b65atagen2026-10-22 07:39 UTC256
            } => {
411bc1c78b65atagen2026-10-22 07:39 UTC257
                let upload = (room_id, related_to);
411bc1c78b65atagen2026-10-22 07:39 UTC258
                if self.ended.contains(&upload) {
411bc1c78b65atagen2026-10-22 07:39 UTC259
                    return None;
411bc1c78b65atagen2026-10-22 07:39 UTC260
                }
411bc1c78b65atagen2026-10-22 07:39 UTC261
                self.live.insert(upload.clone());
411bc1c78b65atagen2026-10-22 07:39 UTC262
                let (room_id, transaction_id) = upload;
411bc1c78b65atagen2026-10-22 07:39 UTC263
                return Some(Event::MediaUploadProgress {
411bc1c78b65atagen2026-10-22 07:39 UTC264
                    room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC265
                    transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC266
                    uploaded_bytes: progress.current as u64,
411bc1c78b65atagen2026-10-22 07:39 UTC267
                    total_bytes: progress.total as u64,
411bc1c78b65atagen2026-10-22 07:39 UTC268
                    complete: file.is_some() && progress.current == progress.total,
411bc1c78b65atagen2026-10-22 07:39 UTC269
                });
411bc1c78b65atagen2026-10-22 07:39 UTC270
            }
7c26fad34822atagen2026-09-26 09:41 UTC271
            RoomSendQueueUpdate::SendError {
0bfa3f46f0beatagen2026-09-26 09:12 UTC272
                transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC273
                error,
7c26fad34822atagen2026-09-26 09:41 UTC274
                is_recoverable,
7c26fad34822atagen2026-09-26 09:41 UTC275
            } => {
7c26fad34822atagen2026-09-26 09:41 UTC276
                let upload = (room_id, transaction_id);
7c26fad34822atagen2026-09-26 09:41 UTC277
                if !self.live.contains(&upload) {
7c26fad34822atagen2026-09-26 09:41 UTC278
                    return None;
7c26fad34822atagen2026-09-26 09:41 UTC279
                }
7c26fad34822atagen2026-09-26 09:41 UTC280
                let (room_id, transaction_id) = upload;
7c26fad34822atagen2026-09-26 09:41 UTC281
                return Some(Event::MediaUploadFailed {
7c26fad34822atagen2026-09-26 09:41 UTC282
                    room_id,
7c26fad34822atagen2026-09-26 09:41 UTC283
                    transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC284
                    kind: classify_sdk_error(&error),
7c26fad34822atagen2026-09-26 09:41 UTC285
                    will_retry: is_recoverable,
7c26fad34822atagen2026-09-26 09:41 UTC286
                });
7c26fad34822atagen2026-09-26 09:41 UTC287
            }
7c26fad34822atagen2026-09-26 09:41 UTC288
            RoomSendQueueUpdate::SentEvent {
7c26fad34822atagen2026-09-26 09:41 UTC289
                transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC290
                event_id,
7c26fad34822atagen2026-09-26 09:41 UTC291
            } => (transaction_id, UploadOutcome::Sent { event_id }),
7c26fad34822atagen2026-09-26 09:41 UTC292
            RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
7c26fad34822atagen2026-09-26 09:41 UTC293
                (transaction_id, UploadOutcome::Cancelled)
7c26fad34822atagen2026-09-26 09:41 UTC294
            }
7c26fad34822atagen2026-09-26 09:41 UTC295
            _ => return None,
7c26fad34822atagen2026-09-26 09:41 UTC296
        };
411bc1c78b65atagen2026-10-22 07:39 UTC297
        self.finish((room_id, transaction_id), outcome)
7c26fad34822atagen2026-09-26 09:41 UTC298
    }
7c26fad34822atagen2026-09-26 09:41 UTC299
7c26fad34822atagen2026-09-26 09:41 UTC300
    fn resnapshot(&mut self, current: HashSet<Upload>) -> Vec<Event> {
7c26fad34822atagen2026-09-26 09:41 UTC301
        let reports = self.finish_where(|upload| !current.contains(upload), &UploadOutcome::Missed);
7c26fad34822atagen2026-09-26 09:41 UTC302
        let ended = &self.ended;
7c26fad34822atagen2026-09-26 09:41 UTC303
        self.live
7c26fad34822atagen2026-09-26 09:41 UTC304
            .extend(current.into_iter().filter(|upload| !ended.contains(upload)));
7c26fad34822atagen2026-09-26 09:41 UTC305
        reports
7c26fad34822atagen2026-09-26 09:41 UTC306
    }
7c26fad34822atagen2026-09-26 09:41 UTC307
7c26fad34822atagen2026-09-26 09:41 UTC308
    fn finish_where(
7c26fad34822atagen2026-09-26 09:41 UTC309
        &mut self,
7c26fad34822atagen2026-09-26 09:41 UTC310
        ends: impl Fn(&Upload) -> bool,
7c26fad34822atagen2026-09-26 09:41 UTC311
        outcome: &UploadOutcome,
7c26fad34822atagen2026-09-26 09:41 UTC312
    ) -> Vec<Event> {
7c26fad34822atagen2026-09-26 09:41 UTC313
        let ending = self
7c26fad34822atagen2026-09-26 09:41 UTC314
            .live
7c26fad34822atagen2026-09-26 09:41 UTC315
            .iter()
7c26fad34822atagen2026-09-26 09:41 UTC316
            .filter(|upload| ends(upload))
7c26fad34822atagen2026-09-26 09:41 UTC317
            .cloned()
7c26fad34822atagen2026-09-26 09:41 UTC318
            .collect::<Vec<_>>();
7c26fad34822atagen2026-09-26 09:41 UTC319
        ending
7c26fad34822atagen2026-09-26 09:41 UTC320
            .into_iter()
411bc1c78b65atagen2026-10-22 07:39 UTC321
            .filter_map(|upload| self.finish(upload, outcome.clone()))
7c26fad34822atagen2026-09-26 09:41 UTC322
            .collect()
7c26fad34822atagen2026-09-26 09:41 UTC323
    }
7c26fad34822atagen2026-09-26 09:41 UTC324
411bc1c78b65atagen2026-10-22 07:39 UTC325
    /// an end for an upload we never saw is remembered too, its ticks may still be coming
411bc1c78b65atagen2026-10-22 07:39 UTC326
    fn finish(&mut self, upload: Upload, outcome: UploadOutcome) -> Option<Event> {
411bc1c78b65atagen2026-10-22 07:39 UTC327
        if self.ended.insert(upload.clone()) {
411bc1c78b65atagen2026-10-22 07:39 UTC328
            self.ended_order.push_back(upload.clone());
411bc1c78b65atagen2026-10-22 07:39 UTC329
            if self.ended_order.len() > REMEMBERED_ENDS
411bc1c78b65atagen2026-10-22 07:39 UTC330
                && let Some(oldest) = self.ended_order.pop_front()
411bc1c78b65atagen2026-10-22 07:39 UTC331
            {
411bc1c78b65atagen2026-10-22 07:39 UTC332
                self.ended.remove(&oldest);
411bc1c78b65atagen2026-10-22 07:39 UTC333
            }
411bc1c78b65atagen2026-10-22 07:39 UTC334
        }
7c26fad34822atagen2026-09-26 09:41 UTC335
        if !self.live.remove(&upload) {
7c26fad34822atagen2026-09-26 09:41 UTC336
            return None;
31ce2453c82fatagen2026-09-24 13:07 UTC337
        }
7c26fad34822atagen2026-09-26 09:41 UTC338
        let (room_id, transaction_id) = upload;
7c26fad34822atagen2026-09-26 09:41 UTC339
        Some(Event::MediaUploadFinished {
7c26fad34822atagen2026-09-26 09:41 UTC340
            room_id,
7c26fad34822atagen2026-09-26 09:41 UTC341
            transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC342
            outcome,
7c26fad34822atagen2026-09-26 09:41 UTC343
        })
31ce2453c82fatagen2026-09-24 13:07 UTC344
    }
0bfa3f46f0beatagen2026-09-26 09:12 UTC345
}
0bfa3f46f0beatagen2026-09-26 09:12 UTC346
7c26fad34822atagen2026-09-26 09:41 UTC347
async fn attachment_echoes(client: &Client) -> HashSet<Upload> {
7c26fad34822atagen2026-09-26 09:41 UTC348
    let Ok(echoes) = client.send_queue().local_echoes().await else {
7c26fad34822atagen2026-09-26 09:41 UTC349
        return HashSet::new();
7c26fad34822atagen2026-09-26 09:41 UTC350
    };
7c26fad34822atagen2026-09-26 09:41 UTC351
    echoes
0bfa3f46f0beatagen2026-09-26 09:12 UTC352
        .into_iter()
7c26fad34822atagen2026-09-26 09:41 UTC353
        .flat_map(|(room_id, echoes)| {
7c26fad34822atagen2026-09-26 09:41 UTC354
            echoes
7c26fad34822atagen2026-09-26 09:41 UTC355
                .into_iter()
7c26fad34822atagen2026-09-26 09:41 UTC356
                .filter(|echo| is_attachment_echo(&echo.content))
7c26fad34822atagen2026-09-26 09:41 UTC357
                .map(move |echo| (room_id.clone(), echo.transaction_id))
0bfa3f46f0beatagen2026-09-26 09:12 UTC358
        })
0bfa3f46f0beatagen2026-09-26 09:12 UTC359
        .collect()
31ce2453c82fatagen2026-09-24 13:07 UTC360
}
31ce2453c82fatagen2026-09-24 13:07 UTC361
31ce2453c82fatagen2026-09-24 13:07 UTC362
fn is_attachment_echo(content: &LocalEchoContent) -> bool {
31ce2453c82fatagen2026-09-24 13:07 UTC363
    let LocalEchoContent::Event {
31ce2453c82fatagen2026-09-24 13:07 UTC364
        serialized_event, ..
31ce2453c82fatagen2026-09-24 13:07 UTC365
    } = content
31ce2453c82fatagen2026-09-24 13:07 UTC366
    else {
31ce2453c82fatagen2026-09-24 13:07 UTC367
        return false;
31ce2453c82fatagen2026-09-24 13:07 UTC368
    };
31ce2453c82fatagen2026-09-24 13:07 UTC369
    matches!(
31ce2453c82fatagen2026-09-24 13:07 UTC370
        serialized_event.deserialize(),
31ce2453c82fatagen2026-09-24 13:07 UTC371
        Ok(AnyMessageLikeEventContent::RoomMessage(content))
31ce2453c82fatagen2026-09-24 13:07 UTC372
            if matches!(
31ce2453c82fatagen2026-09-24 13:07 UTC373
                content.msgtype,
31ce2453c82fatagen2026-09-24 13:07 UTC374
                MessageType::Audio(_) | MessageType::File(_) | MessageType::Image(_) | MessageType::Video(_)
31ce2453c82fatagen2026-09-24 13:07 UTC375
            )
31ce2453c82fatagen2026-09-24 13:07 UTC376
    )
31ce2453c82fatagen2026-09-24 13:07 UTC377
}
31ce2453c82fatagen2026-09-24 13:07 UTC378
743123167e29atagen2026-10-26 04:02 UTC379
pub(super) async fn encryption(
743123167e29atagen2026-10-26 04:02 UTC380
    client: Client,
743123167e29atagen2026-10-26 04:02 UTC381
    events: EventSender,
743123167e29atagen2026-10-26 04:02 UTC382
    mut enabling: watch::Receiver<bool>,
743123167e29atagen2026-10-26 04:02 UTC383
) {
31ce2453c82fatagen2026-09-24 13:07 UTC384
    let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {
31ce2453c82fatagen2026-09-24 13:07 UTC385
        tracing::warn!("encryption watcher stopped because the client has no device id");
31ce2453c82fatagen2026-09-24 13:07 UTC386
        return;
31ce2453c82fatagen2026-09-24 13:07 UTC387
    };
31ce2453c82fatagen2026-09-24 13:07 UTC388
    let recovery = client.encryption().recovery();
31ce2453c82fatagen2026-09-24 13:07 UTC389
    let backups = client.encryption().backups();
13da982fe7bbatagen2026-10-26 07:30 UTC390
    // until this is cached, the timeline labels undecryptable events from a local guess
13da982fe7bbatagen2026-10-26 07:30 UTC391
    matrix_sdk::executor::spawn({
13da982fe7bbatagen2026-10-26 07:30 UTC392
        let backups = backups.clone();
13da982fe7bbatagen2026-10-26 07:30 UTC393
        async move {
13da982fe7bbatagen2026-10-26 07:30 UTC394
            let _ = backups.exists_on_server().await;
13da982fe7bbatagen2026-10-26 07:30 UTC395
        }
13da982fe7bbatagen2026-10-26 07:30 UTC396
    });
31ce2453c82fatagen2026-09-24 13:07 UTC397
    let mut recovery_stream = recovery.state_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC398
    let mut backup_stream = backups.state_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC399
    let mut verification_stream = client.encryption().verification_state();
31ce2453c82fatagen2026-09-24 13:07 UTC400
    let mut recovery_state = recovery.state();
31ce2453c82fatagen2026-09-24 13:07 UTC401
    let mut backup_state = backups.state();
31ce2453c82fatagen2026-09-24 13:07 UTC402
    let mut verification_state = verification_stream.get();
31ce2453c82fatagen2026-09-24 13:07 UTC403
    loop {
743123167e29atagen2026-10-26 04:02 UTC404
        let mut state = encryption_state(
31ce2453c82fatagen2026-09-24 13:07 UTC405
            device_id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC406
            recovery_state,
31ce2453c82fatagen2026-09-24 13:07 UTC407
            verification_state,
31ce2453c82fatagen2026-09-24 13:07 UTC408
            backup_state,
31ce2453c82fatagen2026-09-24 13:07 UTC409
        );
743123167e29atagen2026-10-26 04:02 UTC410
        if *enabling.borrow_and_update() {
743123167e29atagen2026-10-26 04:02 UTC411
            state.recovery = crate::RecoveryState::Enabling;
743123167e29atagen2026-10-26 04:02 UTC412
        }
31ce2453c82fatagen2026-09-24 13:07 UTC413
        if events.send(Event::EncryptionChanged(state)).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC414
            return;
31ce2453c82fatagen2026-09-24 13:07 UTC415
        }
31ce2453c82fatagen2026-09-24 13:07 UTC416
        tokio::select! {
31ce2453c82fatagen2026-09-24 13:07 UTC417
            Some(update) = recovery_stream.next() => recovery_state = update,
743123167e29atagen2026-10-26 04:02 UTC418
            Ok(()) = enabling.changed() => {}
31ce2453c82fatagen2026-09-24 13:07 UTC419
            Some(update) = verification_stream.next() => verification_state = update,
31ce2453c82fatagen2026-09-24 13:07 UTC420
            Some(update) = backup_stream.next() => {
31ce2453c82fatagen2026-09-24 13:07 UTC421
                backup_state = update.unwrap_or_else(|_| backups.state());
31ce2453c82fatagen2026-09-24 13:07 UTC422
            }
31ce2453c82fatagen2026-09-24 13:07 UTC423
            else => return,
31ce2453c82fatagen2026-09-24 13:07 UTC424
        }
31ce2453c82fatagen2026-09-24 13:07 UTC425
    }
31ce2453c82fatagen2026-09-24 13:07 UTC426
}
31ce2453c82fatagen2026-09-24 13:07 UTC427
fa5b4e09943aatagen2026-10-24 09:33 UTC428
/// space orders, pins and power levels all feed summaries without raising a notable room info update
31ce2453c82fatagen2026-09-24 13:07 UTC429
pub(super) async fn rooms(client: Client, events: EventSender) {
31ce2453c82fatagen2026-09-24 13:07 UTC430
    let (_, mut rooms) = client.rooms_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC431
    let mut info = client.room_info_notable_update_receiver();
fa5b4e09943aatagen2026-10-24 09:33 UTC432
    let changed = Arc::new(Notify::new());
fa5b4e09943aatagen2026-10-24 09:33 UTC433
    let handlers = [
fa5b4e09943aatagen2026-10-24 09:33 UTC434
        client.add_event_handler({
fa5b4e09943aatagen2026-10-24 09:33 UTC435
            let changed = changed.clone();
fa5b4e09943aatagen2026-10-24 09:33 UTC436
            move |event: Raw<AnyRoomAccountDataEvent>| {
fa5b4e09943aatagen2026-10-24 09:33 UTC437
                let changed = changed.clone();
fa5b4e09943aatagen2026-10-24 09:33 UTC438
                async move {
fa5b4e09943aatagen2026-10-24 09:33 UTC439
                    if event_type(&event).as_deref() == Some(space_order::EVENT_TYPE) {
fa5b4e09943aatagen2026-10-24 09:33 UTC440
                        changed.notify_one();
fa5b4e09943aatagen2026-10-24 09:33 UTC441
                    }
9bd34e25bb86atagen2026-10-24 07:23 UTC442
                }
9bd34e25bb86atagen2026-10-24 07:23 UTC443
            }
fa5b4e09943aatagen2026-10-24 09:33 UTC444
        }),
fa5b4e09943aatagen2026-10-24 09:33 UTC445
        client.add_event_handler({
fa5b4e09943aatagen2026-10-24 09:33 UTC446
            let changed = changed.clone();
fa5b4e09943aatagen2026-10-24 09:33 UTC447
            move |event: Raw<AnySyncStateEvent>| {
fa5b4e09943aatagen2026-10-24 09:33 UTC448
                let changed = changed.clone();
fa5b4e09943aatagen2026-10-24 09:33 UTC449
                async move {
fa5b4e09943aatagen2026-10-24 09:33 UTC450
                    if matches!(
fa5b4e09943aatagen2026-10-24 09:33 UTC451
                        event_type(&event).as_deref(),
fa5b4e09943aatagen2026-10-24 09:33 UTC452
                        Some("m.room.pinned_events" | "m.room.power_levels")
fa5b4e09943aatagen2026-10-24 09:33 UTC453
                    ) {
fa5b4e09943aatagen2026-10-24 09:33 UTC454
                        changed.notify_one();
fa5b4e09943aatagen2026-10-24 09:33 UTC455
                    }
fa5b4e09943aatagen2026-10-24 09:33 UTC456
                }
fa5b4e09943aatagen2026-10-24 09:33 UTC457
            }
fa5b4e09943aatagen2026-10-24 09:33 UTC458
        }),
fa5b4e09943aatagen2026-10-24 09:33 UTC459
    ];
21b9227729a4atagen2026-10-25 11:03 UTC460
    let (touches, mut touched) = mpsc::unbounded_channel();
21b9227729a4atagen2026-10-25 11:03 UTC461
    let thread_handlers = thread_handlers(&client, &touches);
e5c6427adb35atagen2026-10-25 10:23 UTC462
    let mut timelines = client.event_cache().subscribe_to_room_generic_updates();
e5c6427adb35atagen2026-10-25 10:23 UTC463
    'invalidate: loop {
31ce2453c82fatagen2026-09-24 13:07 UTC464
        if events.send(Event::RoomsChanged).await.is_err() {
9bd34e25bb86atagen2026-10-24 07:23 UTC465
            break;
31ce2453c82fatagen2026-09-24 13:07 UTC466
        }
e5c6427adb35atagen2026-10-25 10:23 UTC467
        // thread events and threaded receipts leave room info alone, so neither reaches `info`
21b9227729a4atagen2026-10-25 11:03 UTC468
        let mut pending = Touched::default();
e5c6427adb35atagen2026-10-25 10:23 UTC469
        loop {
21b9227729a4atagen2026-10-25 11:03 UTC470
            let mut invalidate = tokio::select! {
21b9227729a4atagen2026-10-25 11:03 UTC471
                update = rooms.next() => if update.is_none() { break 'invalidate } else { true },
e5c6427adb35atagen2026-10-25 10:23 UTC472
                update = info.recv() => {
e5c6427adb35atagen2026-10-25 10:23 UTC473
                    if matches!(update, Err(RecvError::Closed)) { break 'invalidate }
21b9227729a4atagen2026-10-25 11:03 UTC474
                    true
21b9227729a4atagen2026-10-25 11:03 UTC475
                }
21b9227729a4atagen2026-10-25 11:03 UTC476
                () = changed.notified() => true,
21b9227729a4atagen2026-10-25 11:03 UTC477
                Some((room_id, root)) = touched.recv() => {
21b9227729a4atagen2026-10-25 11:03 UTC478
                    pending.add(room_id, root);
21b9227729a4atagen2026-10-25 11:03 UTC479
                    false
e5c6427adb35atagen2026-10-25 10:23 UTC480
                }
e5c6427adb35atagen2026-10-25 10:23 UTC481
                update = timelines.recv() => match update {
21b9227729a4atagen2026-10-25 11:03 UTC482
                    Ok(update) => {
21b9227729a4atagen2026-10-25 11:03 UTC483
                        pending.add(update.room_id, None);
21b9227729a4atagen2026-10-25 11:03 UTC484
                        false
e5c6427adb35atagen2026-10-25 10:23 UTC485
                    }
21b9227729a4atagen2026-10-25 11:03 UTC486
                    Err(RecvError::Lagged(_)) => true,
e5c6427adb35atagen2026-10-25 10:23 UTC487
                    Err(RecvError::Closed) => break 'invalidate,
e5c6427adb35atagen2026-10-25 10:23 UTC488
                },
e5c6427adb35atagen2026-10-25 10:23 UTC489
            };
21b9227729a4atagen2026-10-25 11:03 UTC490
            // a sync's room info lands before the event cache has stored its events, so
21b9227729a4atagen2026-10-25 11:03 UTC491
            // give it a moment and take the whole sync as one invalidation
21b9227729a4atagen2026-10-25 11:03 UTC492
            matrix_sdk::sleep::sleep(SYNC_SETTLE).await;
21b9227729a4atagen2026-10-25 11:03 UTC493
            loop {
21b9227729a4atagen2026-10-25 11:03 UTC494
                match info.try_recv() {
21b9227729a4atagen2026-10-25 11:03 UTC495
                    Ok(_) | Err(TryRecvError::Lagged(_)) => invalidate = true,
21b9227729a4atagen2026-10-25 11:03 UTC496
                    Err(_) => break,
21b9227729a4atagen2026-10-25 11:03 UTC497
                }
21b9227729a4atagen2026-10-25 11:03 UTC498
            }
21b9227729a4atagen2026-10-25 11:03 UTC499
            while let Ok((room_id, root)) = touched.try_recv() {
21b9227729a4atagen2026-10-25 11:03 UTC500
                pending.add(room_id, root);
21b9227729a4atagen2026-10-25 11:03 UTC501
            }
21b9227729a4atagen2026-10-25 11:03 UTC502
            loop {
21b9227729a4atagen2026-10-25 11:03 UTC503
                match timelines.try_recv() {
21b9227729a4atagen2026-10-25 11:03 UTC504
                    Ok(update) => pending.add(update.room_id, None),
21b9227729a4atagen2026-10-25 11:03 UTC505
                    Err(TryRecvError::Lagged(_)) => invalidate = true,
21b9227729a4atagen2026-10-25 11:03 UTC506
                    Err(_) => break,
21b9227729a4atagen2026-10-25 11:03 UTC507
                }
21b9227729a4atagen2026-10-25 11:03 UTC508
            }
21b9227729a4atagen2026-10-25 11:03 UTC509
            for (room_id, roots) in pending.0.drain() {
21b9227729a4atagen2026-10-25 11:03 UTC510
                if let Some(room) = client.get_room(&room_id)
21b9227729a4atagen2026-10-25 11:03 UTC511
                    && recount_thread_unreads(&room, roots).await.is_some()
21b9227729a4atagen2026-10-25 11:03 UTC512
                {
21b9227729a4atagen2026-10-25 11:03 UTC513
                    invalidate = true;
21b9227729a4atagen2026-10-25 11:03 UTC514
                }
21b9227729a4atagen2026-10-25 11:03 UTC515
            }
21b9227729a4atagen2026-10-25 11:03 UTC516
            if invalidate {
e5c6427adb35atagen2026-10-25 10:23 UTC517
                break;
e5c6427adb35atagen2026-10-25 10:23 UTC518
            }
31ce2453c82fatagen2026-09-24 13:07 UTC519
        }
31ce2453c82fatagen2026-09-24 13:07 UTC520
    }
21b9227729a4atagen2026-10-25 11:03 UTC521
    for handler in handlers.into_iter().chain(thread_handlers) {
fa5b4e09943aatagen2026-10-24 09:33 UTC522
        client.remove_event_handler(handler);
fa5b4e09943aatagen2026-10-24 09:33 UTC523
    }
31ce2453c82fatagen2026-09-24 13:07 UTC524
}
31ce2453c82fatagen2026-09-24 13:07 UTC525
21b9227729a4atagen2026-10-25 11:03 UTC526
const SYNC_SETTLE: Duration = Duration::from_millis(100);
21b9227729a4atagen2026-10-25 11:03 UTC527
21b9227729a4atagen2026-10-25 11:03 UTC528
/// rooms to recount, with the threads a sync brought news of
e5c6427adb35atagen2026-10-25 10:23 UTC529
#[derive(Default)]
21b9227729a4atagen2026-10-25 11:03 UTC530
struct Touched(HashMap<OwnedRoomId, HashSet<OwnedEventId>>);
e5c6427adb35atagen2026-10-25 10:23 UTC531
21b9227729a4atagen2026-10-25 11:03 UTC532
impl Touched {
21b9227729a4atagen2026-10-25 11:03 UTC533
    fn add(&mut self, room_id: OwnedRoomId, root: Option<OwnedEventId>) {
21b9227729a4atagen2026-10-25 11:03 UTC534
        let roots = self.0.entry(room_id).or_default();
21b9227729a4atagen2026-10-25 11:03 UTC535
        roots.extend(root);
e5c6427adb35atagen2026-10-25 10:23 UTC536
    }
e5c6427adb35atagen2026-10-25 10:23 UTC537
}
e5c6427adb35atagen2026-10-25 10:23 UTC538
21b9227729a4atagen2026-10-25 11:03 UTC539
/// a thread event names its thread before the event cache has it, and only the user's own
21b9227729a4atagen2026-10-25 11:03 UTC540
/// receipts move their counts
21b9227729a4atagen2026-10-25 11:03 UTC541
fn thread_handlers(
21b9227729a4atagen2026-10-25 11:03 UTC542
    client: &Client,
21b9227729a4atagen2026-10-25 11:03 UTC543
    touches: &mpsc::UnboundedSender<(OwnedRoomId, Option<OwnedEventId>)>,
21b9227729a4atagen2026-10-25 11:03 UTC544
) -> [EventHandlerHandle; 2] {
21b9227729a4atagen2026-10-25 11:03 UTC545
    let own_user_id = client.user_id().map(ToOwned::to_owned);
21b9227729a4atagen2026-10-25 11:03 UTC546
    [
21b9227729a4atagen2026-10-25 11:03 UTC547
        client.add_event_handler({
21b9227729a4atagen2026-10-25 11:03 UTC548
            let touches = touches.clone();
21b9227729a4atagen2026-10-25 11:03 UTC549
            move |event: Raw<AnySyncTimelineEvent>, room: Room| {
21b9227729a4atagen2026-10-25 11:03 UTC550
                if let Some(root) = extract_thread_root(&event) {
21b9227729a4atagen2026-10-25 11:03 UTC551
                    let _ = touches.send((room.room_id().to_owned(), Some(root)));
21b9227729a4atagen2026-10-25 11:03 UTC552
                }
21b9227729a4atagen2026-10-25 11:03 UTC553
                async {}
21b9227729a4atagen2026-10-25 11:03 UTC554
            }
21b9227729a4atagen2026-10-25 11:03 UTC555
        }),
21b9227729a4atagen2026-10-25 11:03 UTC556
        client.add_event_handler({
21b9227729a4atagen2026-10-25 11:03 UTC557
            let touches = touches.clone();
21b9227729a4atagen2026-10-25 11:03 UTC558
            move |event: SyncReceiptEvent, room: Room| {
21b9227729a4atagen2026-10-25 11:03 UTC559
                let own = own_user_id.as_ref().is_some_and(|own_user_id| {
21b9227729a4atagen2026-10-25 11:03 UTC560
                    event
21b9227729a4atagen2026-10-25 11:03 UTC561
                        .content
21b9227729a4atagen2026-10-25 11:03 UTC562
                        .0
21b9227729a4atagen2026-10-25 11:03 UTC563
                        .values()
21b9227729a4atagen2026-10-25 11:03 UTC564
                        .flat_map(|receipts| receipts.values())
21b9227729a4atagen2026-10-25 11:03 UTC565
                        .any(|users| users.contains_key(own_user_id))
21b9227729a4atagen2026-10-25 11:03 UTC566
                });
21b9227729a4atagen2026-10-25 11:03 UTC567
                if own {
21b9227729a4atagen2026-10-25 11:03 UTC568
                    let _ = touches.send((room.room_id().to_owned(), None));
21b9227729a4atagen2026-10-25 11:03 UTC569
                }
21b9227729a4atagen2026-10-25 11:03 UTC570
                async {}
21b9227729a4atagen2026-10-25 11:03 UTC571
            }
21b9227729a4atagen2026-10-25 11:03 UTC572
        }),
21b9227729a4atagen2026-10-25 11:03 UTC573
    ]
21b9227729a4atagen2026-10-25 11:03 UTC574
}
21b9227729a4atagen2026-10-25 11:03 UTC575
17e042fbc331atagen2026-10-08 12:39 UTC576
/// packs live in room state and account data, and leaving a room takes its packs with it
17e042fbc331atagen2026-10-08 12:39 UTC577
pub(super) async fn image_packs(client: Client, events: EventSender) {
17e042fbc331atagen2026-10-08 12:39 UTC578
    let changed = Arc::new(Notify::new());
17e042fbc331atagen2026-10-08 12:39 UTC579
    let handlers = [
17e042fbc331atagen2026-10-08 12:39 UTC580
        client.add_event_handler({
17e042fbc331atagen2026-10-08 12:39 UTC581
            let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC582
            move |event: Raw<AnySyncStateEvent>| {
17e042fbc331atagen2026-10-08 12:39 UTC583
                let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC584
                async move {
17e042fbc331atagen2026-10-08 12:39 UTC585
                    if event_type(&event).as_deref() == Some(ROOM_PACKS) {
17e042fbc331atagen2026-10-08 12:39 UTC586
                        changed.notify_one();
17e042fbc331atagen2026-10-08 12:39 UTC587
                    }
17e042fbc331atagen2026-10-08 12:39 UTC588
                }
17e042fbc331atagen2026-10-08 12:39 UTC589
            }
17e042fbc331atagen2026-10-08 12:39 UTC590
        }),
17e042fbc331atagen2026-10-08 12:39 UTC591
        client.add_event_handler({
17e042fbc331atagen2026-10-08 12:39 UTC592
            let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC593
            move |event: Raw<AnyGlobalAccountDataEvent>| {
17e042fbc331atagen2026-10-08 12:39 UTC594
                let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC595
                async move {
17e042fbc331atagen2026-10-08 12:39 UTC596
                    if matches!(
17e042fbc331atagen2026-10-08 12:39 UTC597
                        event_type(&event).as_deref(),
17e042fbc331atagen2026-10-08 12:39 UTC598
                        Some(USER_PACK | ENABLED_ROOMS)
17e042fbc331atagen2026-10-08 12:39 UTC599
                    ) {
17e042fbc331atagen2026-10-08 12:39 UTC600
                        changed.notify_one();
17e042fbc331atagen2026-10-08 12:39 UTC601
                    }
17e042fbc331atagen2026-10-08 12:39 UTC602
                }
17e042fbc331atagen2026-10-08 12:39 UTC603
            }
17e042fbc331atagen2026-10-08 12:39 UTC604
        }),
17e042fbc331atagen2026-10-08 12:39 UTC605
    ];
17e042fbc331atagen2026-10-08 12:39 UTC606
    let mut info = client.room_info_notable_update_receiver();
17e042fbc331atagen2026-10-08 12:39 UTC607
    loop {
17e042fbc331atagen2026-10-08 12:39 UTC608
        tokio::select! {
17e042fbc331atagen2026-10-08 12:39 UTC609
            () = changed.notified() => {}
17e042fbc331atagen2026-10-08 12:39 UTC610
            update = info.recv() => match update {
17e042fbc331atagen2026-10-08 12:39 UTC611
                Ok(update) if !update.reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP) => {
17e042fbc331atagen2026-10-08 12:39 UTC612
                    continue;
17e042fbc331atagen2026-10-08 12:39 UTC613
                }
17e042fbc331atagen2026-10-08 12:39 UTC614
                Ok(_) | Err(RecvError::Lagged(_)) => {}
17e042fbc331atagen2026-10-08 12:39 UTC615
                Err(RecvError::Closed) => break,
17e042fbc331atagen2026-10-08 12:39 UTC616
            },
17e042fbc331atagen2026-10-08 12:39 UTC617
        }
17e042fbc331atagen2026-10-08 12:39 UTC618
        if events.send(Event::ImagePacksChanged).await.is_err() {
17e042fbc331atagen2026-10-08 12:39 UTC619
            break;
17e042fbc331atagen2026-10-08 12:39 UTC620
        }
17e042fbc331atagen2026-10-08 12:39 UTC621
    }
17e042fbc331atagen2026-10-08 12:39 UTC622
    for handler in handlers {
17e042fbc331atagen2026-10-08 12:39 UTC623
        client.remove_event_handler(handler);
17e042fbc331atagen2026-10-08 12:39 UTC624
    }
17e042fbc331atagen2026-10-08 12:39 UTC625
}
17e042fbc331atagen2026-10-08 12:39 UTC626
17e042fbc331atagen2026-10-08 12:39 UTC627
fn event_type<T>(event: &Raw<T>) -> Option<String> {
17e042fbc331atagen2026-10-08 12:39 UTC628
    event.get_field("type").ok().flatten()
17e042fbc331atagen2026-10-08 12:39 UTC629
}
17e042fbc331atagen2026-10-08 12:39 UTC630
31ce2453c82fatagen2026-09-24 13:07 UTC631
pub(super) async fn notification_settings(client: Client, events: EventSender) {
31ce2453c82fatagen2026-09-24 13:07 UTC632
    let settings = client.notification_settings().await;
31ce2453c82fatagen2026-09-24 13:07 UTC633
    let mut changes = settings.subscribe_to_changes();
31ce2453c82fatagen2026-09-24 13:07 UTC634
    while let Ok(()) | Err(RecvError::Lagged(_)) = changes.recv().await {
0bfa3f46f0beatagen2026-09-26 09:12 UTC635
        // room summaries carry the effective mode, and push rule changes never touch room info
0bfa3f46f0beatagen2026-09-26 09:12 UTC636
        for event in [Event::NotificationSettingsChanged, Event::RoomsChanged] {
0bfa3f46f0beatagen2026-09-26 09:12 UTC637
            if events.send(event).await.is_err() {
0bfa3f46f0beatagen2026-09-26 09:12 UTC638
                return;
0bfa3f46f0beatagen2026-09-26 09:12 UTC639
            }
31ce2453c82fatagen2026-09-24 13:07 UTC640
        }
31ce2453c82fatagen2026-09-24 13:07 UTC641
    }
31ce2453c82fatagen2026-09-24 13:07 UTC642
}
7c26fad34822atagen2026-09-26 09:41 UTC643
7c26fad34822atagen2026-09-26 09:41 UTC644
#[cfg(test)]
7c26fad34822atagen2026-09-26 09:41 UTC645
mod tests {
411bc1c78b65atagen2026-10-22 07:39 UTC646
    use matrix_sdk::send_queue::{AbstractProgress, RoomSendQueueUpdate, SendQueueUpdate};
7c26fad34822atagen2026-09-26 09:41 UTC647
    use ruma::{OwnedTransactionId, event_id, owned_room_id};
7c26fad34822atagen2026-09-26 09:41 UTC648
411bc1c78b65atagen2026-10-22 07:39 UTC649
    use super::{HashSet, Upload, UploadTracker};
7c26fad34822atagen2026-09-26 09:41 UTC650
    use crate::{Event, UploadOutcome};
7c26fad34822atagen2026-09-26 09:41 UTC651
7cf348521204atagen2026-09-26 12:28 UTC652
    fn upload() -> Upload {
7cf348521204atagen2026-09-26 12:28 UTC653
        (
7c26fad34822atagen2026-09-26 09:41 UTC654
            owned_room_id!("!room:localhost"),
7c26fad34822atagen2026-09-26 09:41 UTC655
            OwnedTransactionId::from("upload"),
7cf348521204atagen2026-09-26 12:28 UTC656
        )
7cf348521204atagen2026-09-26 12:28 UTC657
    }
7cf348521204atagen2026-09-26 12:28 UTC658
411bc1c78b65atagen2026-10-22 07:39 UTC659
    fn update(update: RoomSendQueueUpdate) -> SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC660
        SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC661
            room_id: upload().0,
411bc1c78b65atagen2026-10-22 07:39 UTC662
            update,
7cf348521204atagen2026-09-26 12:28 UTC663
        }
7cf348521204atagen2026-09-26 12:28 UTC664
    }
7cf348521204atagen2026-09-26 12:28 UTC665
411bc1c78b65atagen2026-10-22 07:39 UTC666
    fn tick() -> SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC667
        update(RoomSendQueueUpdate::MediaUpload {
411bc1c78b65atagen2026-10-22 07:39 UTC668
            related_to: upload().1,
411bc1c78b65atagen2026-10-22 07:39 UTC669
            file: None,
411bc1c78b65atagen2026-10-22 07:39 UTC670
            index: 0,
411bc1c78b65atagen2026-10-22 07:39 UTC671
            progress: AbstractProgress {
411bc1c78b65atagen2026-10-22 07:39 UTC672
                current: 1,
411bc1c78b65atagen2026-10-22 07:39 UTC673
                total: 2,
411bc1c78b65atagen2026-10-22 07:39 UTC674
            },
411bc1c78b65atagen2026-10-22 07:39 UTC675
        })
7cf348521204atagen2026-09-26 12:28 UTC676
    }
7cf348521204atagen2026-09-26 12:28 UTC677
411bc1c78b65atagen2026-10-22 07:39 UTC678
    fn sent() -> SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC679
        update(RoomSendQueueUpdate::SentEvent {
411bc1c78b65atagen2026-10-22 07:39 UTC680
            transaction_id: upload().1,
411bc1c78b65atagen2026-10-22 07:39 UTC681
            event_id: event_id!("$sent:localhost").to_owned(),
411bc1c78b65atagen2026-10-22 07:39 UTC682
        })
7cf348521204atagen2026-09-26 12:28 UTC683
    }
7cf348521204atagen2026-09-26 12:28 UTC684
7cf348521204atagen2026-09-26 12:28 UTC685
    fn finished(event: Option<Event>) -> Option<UploadOutcome> {
7cf348521204atagen2026-09-26 12:28 UTC686
        match event? {
7cf348521204atagen2026-09-26 12:28 UTC687
            Event::MediaUploadFinished { outcome, .. } => Some(outcome),
7cf348521204atagen2026-09-26 12:28 UTC688
            _ => None,
7cf348521204atagen2026-09-26 12:28 UTC689
        }
7cf348521204atagen2026-09-26 12:28 UTC690
    }
7c26fad34822atagen2026-09-26 09:41 UTC691
7cf348521204atagen2026-09-26 12:28 UTC692
    #[test]
411bc1c78b65atagen2026-10-22 07:39 UTC693
    fn a_tick_trailing_its_end_says_nothing() {
411bc1c78b65atagen2026-10-22 07:39 UTC694
        let mut tracker = UploadTracker::new(HashSet::new());
411bc1c78b65atagen2026-10-22 07:39 UTC695
        assert!(tracker.update(tick()).is_some());
411bc1c78b65atagen2026-10-22 07:39 UTC696
        assert!(matches!(
411bc1c78b65atagen2026-10-22 07:39 UTC697
            finished(tracker.update(sent())),
411bc1c78b65atagen2026-10-22 07:39 UTC698
            Some(UploadOutcome::Sent { .. })
411bc1c78b65atagen2026-10-22 07:39 UTC699
        ));
411bc1c78b65atagen2026-10-22 07:39 UTC700
        assert!(tracker.update(tick()).is_none());
411bc1c78b65atagen2026-10-22 07:39 UTC701
        assert!(tracker.live.is_empty());
411bc1c78b65atagen2026-10-22 07:39 UTC702
    }
411bc1c78b65atagen2026-10-22 07:39 UTC703
411bc1c78b65atagen2026-10-22 07:39 UTC704
    #[test]
7cf348521204atagen2026-09-26 12:28 UTC705
    fn a_stale_tick_after_a_missed_end_changes_nothing() {
411bc1c78b65atagen2026-10-22 07:39 UTC706
        let mut tracker = UploadTracker::new(HashSet::new());
411bc1c78b65atagen2026-10-22 07:39 UTC707
        assert!(tracker.update(tick()).is_some());
7c26fad34822atagen2026-09-26 09:41 UTC708
        let missed = tracker.resnapshot(HashSet::new());
7c26fad34822atagen2026-09-26 09:41 UTC709
        assert!(matches!(
7c26fad34822atagen2026-09-26 09:41 UTC710
            missed.as_slice(),
7c26fad34822atagen2026-09-26 09:41 UTC711
            [Event::MediaUploadFinished {
7c26fad34822atagen2026-09-26 09:41 UTC712
                outcome: UploadOutcome::Missed,
7c26fad34822atagen2026-09-26 09:41 UTC713
                ..
7c26fad34822atagen2026-09-26 09:41 UTC714
            }]
7c26fad34822atagen2026-09-26 09:41 UTC715
        ));
411bc1c78b65atagen2026-10-22 07:39 UTC716
        assert!(tracker.update(tick()).is_none());
411bc1c78b65atagen2026-10-22 07:39 UTC717
        assert!(tracker.update(sent()).is_none());
411bc1c78b65atagen2026-10-22 07:39 UTC718
        assert!(tracker.live.is_empty());
7c26fad34822atagen2026-09-26 09:41 UTC719
    }
7c26fad34822atagen2026-09-26 09:41 UTC720
}