atagen

macltok

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

Blame: src/engine/timeline.rs

960ac0721079atagen2026-09-25 00:24 UTC1
use std::{collections::HashSet, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc};
31ce2453c82fatagen2026-09-24 13:07 UTC2
31ce2453c82fatagen2026-09-24 13:07 UTC3
use futures_util::{FutureExt, StreamExt};
31ce2453c82fatagen2026-09-24 13:07 UTC4
use matrix_sdk::{
31ce2453c82fatagen2026-09-24 13:07 UTC5
    room::edit::EditedContent,
31ce2453c82fatagen2026-09-24 13:07 UTC6
    room::reply::{EnforceThread, Reply as SdkReply},
31ce2453c82fatagen2026-09-24 13:07 UTC7
    ruma::{
31ce2453c82fatagen2026-09-24 13:07 UTC8
        api::client::receipt::create_receipt::v3::ReceiptType,
31ce2453c82fatagen2026-09-24 13:07 UTC9
        events::{
31ce2453c82fatagen2026-09-24 13:07 UTC10
            Mentions,
31ce2453c82fatagen2026-09-24 13:07 UTC11
            room::message::{
31ce2453c82fatagen2026-09-24 13:07 UTC12
                AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
31ce2453c82fatagen2026-09-24 13:07 UTC13
            },
31ce2453c82fatagen2026-09-24 13:07 UTC14
        },
31ce2453c82fatagen2026-09-24 13:07 UTC15
    },
31ce2453c82fatagen2026-09-24 13:07 UTC16
};
31ce2453c82fatagen2026-09-24 13:07 UTC17
use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};
31ce2453c82fatagen2026-09-24 13:07 UTC18
use ruma::{OwnedEventId, OwnedUserId};
31ce2453c82fatagen2026-09-24 13:07 UTC19
use tokio::{
31ce2453c82fatagen2026-09-24 13:07 UTC20
    sync::mpsc,
31ce2453c82fatagen2026-09-24 13:07 UTC21
    task::{JoinHandle, JoinSet},
31ce2453c82fatagen2026-09-24 13:07 UTC22
};
31ce2453c82fatagen2026-09-24 13:07 UTC23
31ce2453c82fatagen2026-09-24 13:07 UTC24
use crate::{
960ac0721079atagen2026-09-25 00:24 UTC25
    Error, ErrorKind, Event, EventRef, HistoryState, MessageContent, MessageEditContent,
960ac0721079atagen2026-09-25 00:24 UTC26
    MessageMentions, RichText, TimelineCommand, TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC27
    engine::MediaService,
31ce2453c82fatagen2026-09-24 13:07 UTC28
    normalise::{MediaScope, normalise_timeline, normalise_timeline_diffs},
31ce2453c82fatagen2026-09-24 13:07 UTC29
    transport::{EventSender, Responder, SendEventError},
31ce2453c82fatagen2026-09-24 13:07 UTC30
};
31ce2453c82fatagen2026-09-24 13:07 UTC31
31ce2453c82fatagen2026-09-24 13:07 UTC32
const PAGINATION_QUEUE: usize = 32;
31ce2453c82fatagen2026-09-24 13:07 UTC33
31ce2453c82fatagen2026-09-24 13:07 UTC34
/// long-lived tasks inside a room actor report panics here so the actor can die loudly
31ce2453c82fatagen2026-09-24 13:07 UTC35
#[derive(Clone)]
31ce2453c82fatagen2026-09-24 13:07 UTC36
pub(super) struct Supervisor(mpsc::UnboundedSender<&'static str>);
31ce2453c82fatagen2026-09-24 13:07 UTC37
31ce2453c82fatagen2026-09-24 13:07 UTC38
impl Supervisor {
31ce2453c82fatagen2026-09-24 13:07 UTC39
    pub(super) fn new() -> (Self, mpsc::UnboundedReceiver<&'static str>) {
31ce2453c82fatagen2026-09-24 13:07 UTC40
        let (sender, receiver) = mpsc::unbounded_channel();
31ce2453c82fatagen2026-09-24 13:07 UTC41
        (Self(sender), receiver)
31ce2453c82fatagen2026-09-24 13:07 UTC42
    }
31ce2453c82fatagen2026-09-24 13:07 UTC43
31ce2453c82fatagen2026-09-24 13:07 UTC44
    pub(super) fn spawn(
31ce2453c82fatagen2026-09-24 13:07 UTC45
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC46
        label: &'static str,
31ce2453c82fatagen2026-09-24 13:07 UTC47
        task: impl Future<Output = ()> + Send + 'static,
31ce2453c82fatagen2026-09-24 13:07 UTC48
    ) -> JoinHandle<()> {
31ce2453c82fatagen2026-09-24 13:07 UTC49
        let panics = self.0.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC50
        tokio::spawn(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC51
            if AssertUnwindSafe(task).catch_unwind().await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC52
                let _ = panics.send(label);
31ce2453c82fatagen2026-09-24 13:07 UTC53
            }
31ce2453c82fatagen2026-09-24 13:07 UTC54
        })
31ce2453c82fatagen2026-09-24 13:07 UTC55
    }
31ce2453c82fatagen2026-09-24 13:07 UTC56
}
31ce2453c82fatagen2026-09-24 13:07 UTC57
31ce2453c82fatagen2026-09-24 13:07 UTC58
/// what a timeline session needs from the room that owns it
31ce2453c82fatagen2026-09-24 13:07 UTC59
#[derive(Clone)]
31ce2453c82fatagen2026-09-24 13:07 UTC60
pub(super) struct SessionContext {
31ce2453c82fatagen2026-09-24 13:07 UTC61
    pub(super) events: EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC62
    pub(super) media: Arc<MediaService>,
31ce2453c82fatagen2026-09-24 13:07 UTC63
    pub(super) current_user_id: Option<OwnedUserId>,
31ce2453c82fatagen2026-09-24 13:07 UTC64
    pub(super) initial_history_limit: u16,
31ce2453c82fatagen2026-09-24 13:07 UTC65
    pub(super) supervisor: Supervisor,
31ce2453c82fatagen2026-09-24 13:07 UTC66
}
31ce2453c82fatagen2026-09-24 13:07 UTC67
31ce2453c82fatagen2026-09-24 13:07 UTC68
pub(super) struct TimelineSession {
31ce2453c82fatagen2026-09-24 13:07 UTC69
    pub(super) id: TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC70
    timeline: Arc<Timeline>,
31ce2453c82fatagen2026-09-24 13:07 UTC71
    context: SessionContext,
31ce2453c82fatagen2026-09-24 13:07 UTC72
    scope: MediaScope,
31ce2453c82fatagen2026-09-24 13:07 UTC73
    watcher: JoinHandle<()>,
31ce2453c82fatagen2026-09-24 13:07 UTC74
    pagination: mpsc::Sender<PaginationRequest>,
31ce2453c82fatagen2026-09-24 13:07 UTC75
    pagination_worker: JoinHandle<()>,
31ce2453c82fatagen2026-09-24 13:07 UTC76
}
31ce2453c82fatagen2026-09-24 13:07 UTC77
31ce2453c82fatagen2026-09-24 13:07 UTC78
struct PaginationRequest {
31ce2453c82fatagen2026-09-24 13:07 UTC79
    limit: u16,
31ce2453c82fatagen2026-09-24 13:07 UTC80
    reply: Option<Responder>,
31ce2453c82fatagen2026-09-24 13:07 UTC81
}
31ce2453c82fatagen2026-09-24 13:07 UTC82
31ce2453c82fatagen2026-09-24 13:07 UTC83
impl TimelineSession {
31ce2453c82fatagen2026-09-24 13:07 UTC84
    pub(super) async fn start(
31ce2453c82fatagen2026-09-24 13:07 UTC85
        id: TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC86
        timeline: Timeline,
31ce2453c82fatagen2026-09-24 13:07 UTC87
        context: SessionContext,
31ce2453c82fatagen2026-09-24 13:07 UTC88
    ) -> Result<Self, SendEventError> {
31ce2453c82fatagen2026-09-24 13:07 UTC89
        let timeline = Arc::new(timeline);
31ce2453c82fatagen2026-09-24 13:07 UTC90
        let scope = MediaScope::new();
31ce2453c82fatagen2026-09-24 13:07 UTC91
        let watcher = start_watcher(&id, &timeline, &context, &scope).await?;
31ce2453c82fatagen2026-09-24 13:07 UTC92
        let (pagination, requests) = mpsc::channel(PAGINATION_QUEUE);
31ce2453c82fatagen2026-09-24 13:07 UTC93
        let pagination_worker = context.supervisor.spawn(
31ce2453c82fatagen2026-09-24 13:07 UTC94
            "pagination worker",
31ce2453c82fatagen2026-09-24 13:07 UTC95
            run_pagination(
31ce2453c82fatagen2026-09-24 13:07 UTC96
                requests,
31ce2453c82fatagen2026-09-24 13:07 UTC97
                timeline.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC98
                context.events.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC99
                id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC100
            ),
31ce2453c82fatagen2026-09-24 13:07 UTC101
        );
31ce2453c82fatagen2026-09-24 13:07 UTC102
        let session = Self {
31ce2453c82fatagen2026-09-24 13:07 UTC103
            id,
31ce2453c82fatagen2026-09-24 13:07 UTC104
            timeline,
31ce2453c82fatagen2026-09-24 13:07 UTC105
            scope,
31ce2453c82fatagen2026-09-24 13:07 UTC106
            watcher,
31ce2453c82fatagen2026-09-24 13:07 UTC107
            pagination,
31ce2453c82fatagen2026-09-24 13:07 UTC108
            pagination_worker,
31ce2453c82fatagen2026-09-24 13:07 UTC109
            context,
31ce2453c82fatagen2026-09-24 13:07 UTC110
        };
31ce2453c82fatagen2026-09-24 13:07 UTC111
        if session.context.initial_history_limit > 0 {
31ce2453c82fatagen2026-09-24 13:07 UTC112
            session.paginate(session.context.initial_history_limit, None);
31ce2453c82fatagen2026-09-24 13:07 UTC113
        }
31ce2453c82fatagen2026-09-24 13:07 UTC114
        Ok(session)
31ce2453c82fatagen2026-09-24 13:07 UTC115
    }
31ce2453c82fatagen2026-09-24 13:07 UTC116
31ce2453c82fatagen2026-09-24 13:07 UTC117
    /// swaps in a fresh watcher. the old one is dead before the reset goes out,
31ce2453c82fatagen2026-09-24 13:07 UTC118
    /// so no stale diff can land after it
31ce2453c82fatagen2026-09-24 13:07 UTC119
    pub(super) async fn resync(&mut self) -> Result<(), Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC120
        self.watcher.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC121
        let _ = (&mut self.watcher).await;
31ce2453c82fatagen2026-09-24 13:07 UTC122
        self.watcher = start_watcher(&self.id, &self.timeline, &self.context, &self.scope)
31ce2453c82fatagen2026-09-24 13:07 UTC123
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC124
            .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;
31ce2453c82fatagen2026-09-24 13:07 UTC125
        Ok(())
31ce2453c82fatagen2026-09-24 13:07 UTC126
    }
31ce2453c82fatagen2026-09-24 13:07 UTC127
31ce2453c82fatagen2026-09-24 13:07 UTC128
    pub(super) fn paginate(&self, limit: u16, reply: Option<Responder>) {
31ce2453c82fatagen2026-09-24 13:07 UTC129
        if let Err(error) = self.pagination.try_send(PaginationRequest { limit, reply }) {
31ce2453c82fatagen2026-09-24 13:07 UTC130
            let (request, error) = match error {
31ce2453c82fatagen2026-09-24 13:07 UTC131
                mpsc::error::TrySendError::Full(request) => (
31ce2453c82fatagen2026-09-24 13:07 UTC132
                    request,
31ce2453c82fatagen2026-09-24 13:07 UTC133
                    Error::new(ErrorKind::QueueFull, "pagination queue is full"),
31ce2453c82fatagen2026-09-24 13:07 UTC134
                ),
31ce2453c82fatagen2026-09-24 13:07 UTC135
                mpsc::error::TrySendError::Closed(request) => (
31ce2453c82fatagen2026-09-24 13:07 UTC136
                    request,
31ce2453c82fatagen2026-09-24 13:07 UTC137
                    Error::new(ErrorKind::TimelineNotOpen, "timeline is closing"),
31ce2453c82fatagen2026-09-24 13:07 UTC138
                ),
31ce2453c82fatagen2026-09-24 13:07 UTC139
            };
31ce2453c82fatagen2026-09-24 13:07 UTC140
            if let Some(reply) = request.reply {
31ce2453c82fatagen2026-09-24 13:07 UTC141
                reply.done(Err(error));
31ce2453c82fatagen2026-09-24 13:07 UTC142
            }
31ce2453c82fatagen2026-09-24 13:07 UTC143
        }
31ce2453c82fatagen2026-09-24 13:07 UTC144
    }
31ce2453c82fatagen2026-09-24 13:07 UTC145
31ce2453c82fatagen2026-09-24 13:07 UTC146
    /// pending paginations answer `Interrupted` as their responders drop
31ce2453c82fatagen2026-09-24 13:07 UTC147
    pub(super) async fn close(mut self) {
31ce2453c82fatagen2026-09-24 13:07 UTC148
        self.context.media.release(&self.scope).await;
31ce2453c82fatagen2026-09-24 13:07 UTC149
        self.pagination_worker.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC150
        let _ = (&mut self.pagination_worker).await;
31ce2453c82fatagen2026-09-24 13:07 UTC151
        self.watcher.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC152
        let _ = (&mut self.watcher).await;
31ce2453c82fatagen2026-09-24 13:07 UTC153
    }
31ce2453c82fatagen2026-09-24 13:07 UTC154
31ce2453c82fatagen2026-09-24 13:07 UTC155
    pub(super) async fn execute(
31ce2453c82fatagen2026-09-24 13:07 UTC156
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC157
        command: TimelineCommand,
31ce2453c82fatagen2026-09-24 13:07 UTC158
        reply: Responder,
31ce2453c82fatagen2026-09-24 13:07 UTC159
        work: &mut JoinSet<()>,
31ce2453c82fatagen2026-09-24 13:07 UTC160
    ) {
31ce2453c82fatagen2026-09-24 13:07 UTC161
        let timeline = self.timeline.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC162
        let thread_root = self.id.thread_root().cloned();
31ce2453c82fatagen2026-09-24 13:07 UTC163
        let background: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> = match command {
31ce2453c82fatagen2026-09-24 13:07 UTC164
            TimelineCommand::Paginate { limit, .. } => return self.paginate(limit, Some(reply)),
31ce2453c82fatagen2026-09-24 13:07 UTC165
            // network-bound, so it runs beside the actor instead of queueing the room behind it
960ac0721079atagen2026-09-25 00:24 UTC166
            TimelineCommand::Redact { target, reason, .. } => Box::pin(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC167
                timeline
960ac0721079atagen2026-09-25 00:24 UTC168
                    .redact(&item_id(target), reason.as_deref())
31ce2453c82fatagen2026-09-24 13:07 UTC169
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC170
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC171
            }),
960ac0721079atagen2026-09-25 00:24 UTC172
            TimelineCommand::ToggleReaction { target, key, .. } => Box::pin(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC173
                timeline
960ac0721079atagen2026-09-25 00:24 UTC174
                    .toggle_reaction(&item_id(target), &key)
31ce2453c82fatagen2026-09-24 13:07 UTC175
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC176
                    .map(drop)
31ce2453c82fatagen2026-09-24 13:07 UTC177
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC178
            }),
31ce2453c82fatagen2026-09-24 13:07 UTC179
            TimelineCommand::MarkRead { event_id, .. } => Box::pin(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC180
                let receipt_failed =
31ce2453c82fatagen2026-09-24 13:07 UTC181
                    |source| Error::from_sdk("sending read receipt failed", source);
31ce2453c82fatagen2026-09-24 13:07 UTC182
                timeline
31ce2453c82fatagen2026-09-24 13:07 UTC183
                    .send_single_receipt(ReceiptType::Read, event_id.clone())
31ce2453c82fatagen2026-09-24 13:07 UTC184
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC185
                    .map_err(receipt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC186
                if thread_root.is_none() {
31ce2453c82fatagen2026-09-24 13:07 UTC187
                    timeline
31ce2453c82fatagen2026-09-24 13:07 UTC188
                        .send_single_receipt(ReceiptType::FullyRead, event_id)
31ce2453c82fatagen2026-09-24 13:07 UTC189
                        .await
31ce2453c82fatagen2026-09-24 13:07 UTC190
                        .map_err(receipt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC191
                }
31ce2453c82fatagen2026-09-24 13:07 UTC192
                Ok(())
31ce2453c82fatagen2026-09-24 13:07 UTC193
            }),
31ce2453c82fatagen2026-09-24 13:07 UTC194
            command => return reply.done(self.execute_serial(command).await),
31ce2453c82fatagen2026-09-24 13:07 UTC195
        };
31ce2453c82fatagen2026-09-24 13:07 UTC196
        work.spawn(async move { reply.done(background.await) });
31ce2453c82fatagen2026-09-24 13:07 UTC197
    }
31ce2453c82fatagen2026-09-24 13:07 UTC198
31ce2453c82fatagen2026-09-24 13:07 UTC199
    /// sends stay on the actor so they hit the send queue in the order they were asked for
31ce2453c82fatagen2026-09-24 13:07 UTC200
    async fn execute_serial(&self, command: TimelineCommand) -> Result<(), Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC201
        let timeline = &self.timeline;
31ce2453c82fatagen2026-09-24 13:07 UTC202
        match command {
31ce2453c82fatagen2026-09-24 13:07 UTC203
            TimelineCommand::Send { content, .. } => {
31ce2453c82fatagen2026-09-24 13:07 UTC204
                let body = match &content {
31ce2453c82fatagen2026-09-24 13:07 UTC205
                    MessageContent::Text { content, .. }
31ce2453c82fatagen2026-09-24 13:07 UTC206
                    | MessageContent::Emote { content, .. }
31ce2453c82fatagen2026-09-24 13:07 UTC207
                    | MessageContent::Reply { content, .. } => &content.body,
31ce2453c82fatagen2026-09-24 13:07 UTC208
                };
31ce2453c82fatagen2026-09-24 13:07 UTC209
                if body.trim().is_empty() {
31ce2453c82fatagen2026-09-24 13:07 UTC210
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
31ce2453c82fatagen2026-09-24 13:07 UTC211
                }
31ce2453c82fatagen2026-09-24 13:07 UTC212
                let content = match content {
31ce2453c82fatagen2026-09-24 13:07 UTC213
                    MessageContent::Text { content, mentions } => {
31ce2453c82fatagen2026-09-24 13:07 UTC214
                        message_content(content, mentions, false).with_relation(None)
31ce2453c82fatagen2026-09-24 13:07 UTC215
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC216
                    MessageContent::Emote { content, mentions } => {
31ce2453c82fatagen2026-09-24 13:07 UTC217
                        message_content(content, mentions, true).with_relation(None)
31ce2453c82fatagen2026-09-24 13:07 UTC218
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC219
                    MessageContent::Reply {
31ce2453c82fatagen2026-09-24 13:07 UTC220
                        event_id,
31ce2453c82fatagen2026-09-24 13:07 UTC221
                        content,
31ce2453c82fatagen2026-09-24 13:07 UTC222
                        mentions,
31ce2453c82fatagen2026-09-24 13:07 UTC223
                        notify_replied_to,
31ce2453c82fatagen2026-09-24 13:07 UTC224
                    } => {
31ce2453c82fatagen2026-09-24 13:07 UTC225
                        self.reply_content(event_id, content, mentions, notify_replied_to)
31ce2453c82fatagen2026-09-24 13:07 UTC226
                            .await?
31ce2453c82fatagen2026-09-24 13:07 UTC227
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC228
                };
31ce2453c82fatagen2026-09-24 13:07 UTC229
                timeline
31ce2453c82fatagen2026-09-24 13:07 UTC230
                    .send(content.into())
31ce2453c82fatagen2026-09-24 13:07 UTC231
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC232
                    .map(drop)
31ce2453c82fatagen2026-09-24 13:07 UTC233
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC234
            }
31ce2453c82fatagen2026-09-24 13:07 UTC235
            TimelineCommand::Edit {
960ac0721079atagen2026-09-25 00:24 UTC236
                target, content, ..
31ce2453c82fatagen2026-09-24 13:07 UTC237
            } => {
31ce2453c82fatagen2026-09-24 13:07 UTC238
                let (content, mentions, emote) = match content {
31ce2453c82fatagen2026-09-24 13:07 UTC239
                    MessageEditContent::Text { content, mentions } => (content, mentions, false),
31ce2453c82fatagen2026-09-24 13:07 UTC240
                    MessageEditContent::Emote { content, mentions } => (content, mentions, true),
31ce2453c82fatagen2026-09-24 13:07 UTC241
                };
31ce2453c82fatagen2026-09-24 13:07 UTC242
                if content.body.trim().is_empty() {
31ce2453c82fatagen2026-09-24 13:07 UTC243
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
31ce2453c82fatagen2026-09-24 13:07 UTC244
                }
31ce2453c82fatagen2026-09-24 13:07 UTC245
                timeline
31ce2453c82fatagen2026-09-24 13:07 UTC246
                    .edit(
960ac0721079atagen2026-09-25 00:24 UTC247
                        &item_id(target),
31ce2453c82fatagen2026-09-24 13:07 UTC248
                        EditedContent::RoomMessage(message_content(content, mentions, emote)),
31ce2453c82fatagen2026-09-24 13:07 UTC249
                    )
31ce2453c82fatagen2026-09-24 13:07 UTC250
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC251
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC252
            }
31ce2453c82fatagen2026-09-24 13:07 UTC253
            _ => Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC254
                ErrorKind::Other,
31ce2453c82fatagen2026-09-24 13:07 UTC255
                "timeline command was misrouted",
31ce2453c82fatagen2026-09-24 13:07 UTC256
            )),
31ce2453c82fatagen2026-09-24 13:07 UTC257
        }
31ce2453c82fatagen2026-09-24 13:07 UTC258
    }
31ce2453c82fatagen2026-09-24 13:07 UTC259
31ce2453c82fatagen2026-09-24 13:07 UTC260
    async fn reply_content(
31ce2453c82fatagen2026-09-24 13:07 UTC261
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC262
        event_id: OwnedEventId,
31ce2453c82fatagen2026-09-24 13:07 UTC263
        content: RichText,
31ce2453c82fatagen2026-09-24 13:07 UTC264
        mentions: MessageMentions,
31ce2453c82fatagen2026-09-24 13:07 UTC265
        notify_replied_to: bool,
31ce2453c82fatagen2026-09-24 13:07 UTC266
    ) -> Result<matrix_sdk::ruma::events::room::message::RoomMessageEventContent, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC267
        let thread_root = self.id.thread_root();
31ce2453c82fatagen2026-09-24 13:07 UTC268
        let content = self
31ce2453c82fatagen2026-09-24 13:07 UTC269
            .timeline
31ce2453c82fatagen2026-09-24 13:07 UTC270
            .room()
31ce2453c82fatagen2026-09-24 13:07 UTC271
            .make_reply_event(
31ce2453c82fatagen2026-09-24 13:07 UTC272
                message_content(content, mentions, false),
31ce2453c82fatagen2026-09-24 13:07 UTC273
                SdkReply {
31ce2453c82fatagen2026-09-24 13:07 UTC274
                    event_id,
31ce2453c82fatagen2026-09-24 13:07 UTC275
                    enforce_thread: if thread_root.is_some() {
31ce2453c82fatagen2026-09-24 13:07 UTC276
                        EnforceThread::Threaded(ReplyWithinThread::Yes)
31ce2453c82fatagen2026-09-24 13:07 UTC277
                    } else {
31ce2453c82fatagen2026-09-24 13:07 UTC278
                        EnforceThread::MaybeThreaded
31ce2453c82fatagen2026-09-24 13:07 UTC279
                    },
31ce2453c82fatagen2026-09-24 13:07 UTC280
                    add_mentions: if notify_replied_to {
31ce2453c82fatagen2026-09-24 13:07 UTC281
                        AddMentions::Yes
31ce2453c82fatagen2026-09-24 13:07 UTC282
                    } else {
31ce2453c82fatagen2026-09-24 13:07 UTC283
                        AddMentions::No
31ce2453c82fatagen2026-09-24 13:07 UTC284
                    },
31ce2453c82fatagen2026-09-24 13:07 UTC285
                },
31ce2453c82fatagen2026-09-24 13:07 UTC286
            )
31ce2453c82fatagen2026-09-24 13:07 UTC287
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC288
            .map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC289
                Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC290
            })?;
31ce2453c82fatagen2026-09-24 13:07 UTC291
        if let Some(thread_root) = thread_root
31ce2453c82fatagen2026-09-24 13:07 UTC292
            && !matches!(
31ce2453c82fatagen2026-09-24 13:07 UTC293
                content.relates_to.as_ref(),
31ce2453c82fatagen2026-09-24 13:07 UTC294
                Some(Relation::Thread(thread)) if &thread.event_id == thread_root
31ce2453c82fatagen2026-09-24 13:07 UTC295
            )
31ce2453c82fatagen2026-09-24 13:07 UTC296
        {
31ce2453c82fatagen2026-09-24 13:07 UTC297
            return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC298
                ErrorKind::Invalid,
31ce2453c82fatagen2026-09-24 13:07 UTC299
                "reply target does not belong to the selected thread",
31ce2453c82fatagen2026-09-24 13:07 UTC300
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC301
        }
31ce2453c82fatagen2026-09-24 13:07 UTC302
        Ok(content)
31ce2453c82fatagen2026-09-24 13:07 UTC303
    }
31ce2453c82fatagen2026-09-24 13:07 UTC304
}
31ce2453c82fatagen2026-09-24 13:07 UTC305
31ce2453c82fatagen2026-09-24 13:07 UTC306
impl Drop for TimelineSession {
31ce2453c82fatagen2026-09-24 13:07 UTC307
    fn drop(&mut self) {
31ce2453c82fatagen2026-09-24 13:07 UTC308
        self.watcher.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC309
        self.pagination_worker.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC310
    }
31ce2453c82fatagen2026-09-24 13:07 UTC311
}
31ce2453c82fatagen2026-09-24 13:07 UTC312
960ac0721079atagen2026-09-25 00:24 UTC313
fn item_id(target: EventRef) -> TimelineEventItemId {
960ac0721079atagen2026-09-25 00:24 UTC314
    match target {
960ac0721079atagen2026-09-25 00:24 UTC315
        EventRef::Remote(event_id) => TimelineEventItemId::EventId(event_id),
960ac0721079atagen2026-09-25 00:24 UTC316
        EventRef::Local(transaction_id) => TimelineEventItemId::TransactionId(transaction_id),
960ac0721079atagen2026-09-25 00:24 UTC317
    }
960ac0721079atagen2026-09-25 00:24 UTC318
}
960ac0721079atagen2026-09-25 00:24 UTC319
960ac0721079atagen2026-09-25 00:24 UTC320
/// the sdk leaves a reply's target unloaded until someone asks, so the watcher asks, once per event
960ac0721079atagen2026-09-25 00:24 UTC321
struct ReplyFetcher {
960ac0721079atagen2026-09-25 00:24 UTC322
    timeline: Arc<Timeline>,
960ac0721079atagen2026-09-25 00:24 UTC323
    asked: HashSet<OwnedEventId>,
960ac0721079atagen2026-09-25 00:24 UTC324
    fetches: JoinSet<()>,
960ac0721079atagen2026-09-25 00:24 UTC325
}
960ac0721079atagen2026-09-25 00:24 UTC326
960ac0721079atagen2026-09-25 00:24 UTC327
impl ReplyFetcher {
960ac0721079atagen2026-09-25 00:24 UTC328
    fn fetch(&mut self, missing: Vec<OwnedEventId>) {
960ac0721079atagen2026-09-25 00:24 UTC329
        for event_id in missing {
960ac0721079atagen2026-09-25 00:24 UTC330
            if self.asked.insert(event_id.clone()) {
960ac0721079atagen2026-09-25 00:24 UTC331
                let timeline = self.timeline.clone();
960ac0721079atagen2026-09-25 00:24 UTC332
                self.fetches.spawn(async move {
960ac0721079atagen2026-09-25 00:24 UTC333
                    if let Err(error) = timeline.fetch_details_for_event(&event_id).await {
960ac0721079atagen2026-09-25 00:24 UTC334
                        tracing::debug!(%event_id, %error, "fetching reply details failed");
960ac0721079atagen2026-09-25 00:24 UTC335
                    }
960ac0721079atagen2026-09-25 00:24 UTC336
                });
960ac0721079atagen2026-09-25 00:24 UTC337
            }
960ac0721079atagen2026-09-25 00:24 UTC338
        }
960ac0721079atagen2026-09-25 00:24 UTC339
    }
960ac0721079atagen2026-09-25 00:24 UTC340
}
960ac0721079atagen2026-09-25 00:24 UTC341
31ce2453c82fatagen2026-09-24 13:07 UTC342
async fn start_watcher(
31ce2453c82fatagen2026-09-24 13:07 UTC343
    id: &TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC344
    timeline: &Arc<Timeline>,
31ce2453c82fatagen2026-09-24 13:07 UTC345
    context: &SessionContext,
31ce2453c82fatagen2026-09-24 13:07 UTC346
    scope: &MediaScope,
31ce2453c82fatagen2026-09-24 13:07 UTC347
) -> Result<JoinHandle<()>, SendEventError> {
31ce2453c82fatagen2026-09-24 13:07 UTC348
    let (items, mut stream) = timeline.subscribe().await;
31ce2453c82fatagen2026-09-24 13:07 UTC349
    let current_user_id = context.current_user_id.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC350
    let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), scope);
31ce2453c82fatagen2026-09-24 13:07 UTC351
    context.media.register(normalised.media).await;
31ce2453c82fatagen2026-09-24 13:07 UTC352
    context
31ce2453c82fatagen2026-09-24 13:07 UTC353
        .events
31ce2453c82fatagen2026-09-24 13:07 UTC354
        .send(Event::TimelineReset {
31ce2453c82fatagen2026-09-24 13:07 UTC355
            timeline_id: id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC356
            items: normalised.items,
31ce2453c82fatagen2026-09-24 13:07 UTC357
        })
31ce2453c82fatagen2026-09-24 13:07 UTC358
        .await?;
960ac0721079atagen2026-09-25 00:24 UTC359
    let mut replies = ReplyFetcher {
960ac0721079atagen2026-09-25 00:24 UTC360
        timeline: timeline.clone(),
960ac0721079atagen2026-09-25 00:24 UTC361
        asked: HashSet::new(),
960ac0721079atagen2026-09-25 00:24 UTC362
        fetches: JoinSet::new(),
960ac0721079atagen2026-09-25 00:24 UTC363
    };
960ac0721079atagen2026-09-25 00:24 UTC364
    replies.fetch(normalised.missing_replies);
31ce2453c82fatagen2026-09-24 13:07 UTC365
    let id = id.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC366
    let events = context.events.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC367
    let media = context.media.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC368
    let scope = scope.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC369
    Ok(context.supervisor.spawn("timeline watcher", async move {
960ac0721079atagen2026-09-25 00:24 UTC370
        loop {
960ac0721079atagen2026-09-25 00:24 UTC371
            let diffs = tokio::select! {
960ac0721079atagen2026-09-25 00:24 UTC372
                diffs = stream.next() => match diffs {
960ac0721079atagen2026-09-25 00:24 UTC373
                    Some(diffs) => diffs,
960ac0721079atagen2026-09-25 00:24 UTC374
                    None => return,
960ac0721079atagen2026-09-25 00:24 UTC375
                },
960ac0721079atagen2026-09-25 00:24 UTC376
                Some(_) = replies.fetches.join_next(), if !replies.fetches.is_empty() => continue,
960ac0721079atagen2026-09-25 00:24 UTC377
            };
31ce2453c82fatagen2026-09-24 13:07 UTC378
            let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);
31ce2453c82fatagen2026-09-24 13:07 UTC379
            media.register(normalised.media).await;
960ac0721079atagen2026-09-25 00:24 UTC380
            replies.fetch(normalised.missing_replies);
31ce2453c82fatagen2026-09-24 13:07 UTC381
            let event = Event::TimelineChanged {
31ce2453c82fatagen2026-09-24 13:07 UTC382
                timeline_id: id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC383
                diffs: normalised.diffs,
31ce2453c82fatagen2026-09-24 13:07 UTC384
            };
31ce2453c82fatagen2026-09-24 13:07 UTC385
            if events.send(event).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC386
                return;
31ce2453c82fatagen2026-09-24 13:07 UTC387
            }
31ce2453c82fatagen2026-09-24 13:07 UTC388
        }
31ce2453c82fatagen2026-09-24 13:07 UTC389
    }))
31ce2453c82fatagen2026-09-24 13:07 UTC390
}
31ce2453c82fatagen2026-09-24 13:07 UTC391
31ce2453c82fatagen2026-09-24 13:07 UTC392
async fn run_pagination(
31ce2453c82fatagen2026-09-24 13:07 UTC393
    mut requests: mpsc::Receiver<PaginationRequest>,
31ce2453c82fatagen2026-09-24 13:07 UTC394
    timeline: Arc<Timeline>,
31ce2453c82fatagen2026-09-24 13:07 UTC395
    events: EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC396
    timeline_id: TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC397
) {
31ce2453c82fatagen2026-09-24 13:07 UTC398
    while let Some(PaginationRequest { limit, reply }) = requests.recv().await {
31ce2453c82fatagen2026-09-24 13:07 UTC399
        let history = |state| Event::HistoryChanged {
31ce2453c82fatagen2026-09-24 13:07 UTC400
            timeline_id: timeline_id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC401
            state,
31ce2453c82fatagen2026-09-24 13:07 UTC402
        };
31ce2453c82fatagen2026-09-24 13:07 UTC403
        if events.send(history(HistoryState::Loading)).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC404
            return;
31ce2453c82fatagen2026-09-24 13:07 UTC405
        }
31ce2453c82fatagen2026-09-24 13:07 UTC406
        let result = timeline
31ce2453c82fatagen2026-09-24 13:07 UTC407
            .paginate_backwards(limit.max(1))
31ce2453c82fatagen2026-09-24 13:07 UTC408
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC409
            .map_err(timeline_error);
31ce2453c82fatagen2026-09-24 13:07 UTC410
        let state = match &result {
31ce2453c82fatagen2026-09-24 13:07 UTC411
            Ok(true) => HistoryState::StartReached,
31ce2453c82fatagen2026-09-24 13:07 UTC412
            Ok(false) => HistoryState::Idle,
31ce2453c82fatagen2026-09-24 13:07 UTC413
            Err(error) => HistoryState::Failed(error.to_string()),
31ce2453c82fatagen2026-09-24 13:07 UTC414
        };
31ce2453c82fatagen2026-09-24 13:07 UTC415
        let _ = events.send(history(state)).await;
31ce2453c82fatagen2026-09-24 13:07 UTC416
        if let Some(reply) = reply {
31ce2453c82fatagen2026-09-24 13:07 UTC417
            reply.done(result.map(drop));
31ce2453c82fatagen2026-09-24 13:07 UTC418
        }
31ce2453c82fatagen2026-09-24 13:07 UTC419
    }
31ce2453c82fatagen2026-09-24 13:07 UTC420
}
31ce2453c82fatagen2026-09-24 13:07 UTC421
31ce2453c82fatagen2026-09-24 13:07 UTC422
fn message_content(
31ce2453c82fatagen2026-09-24 13:07 UTC423
    content: RichText,
31ce2453c82fatagen2026-09-24 13:07 UTC424
    mentions: MessageMentions,
31ce2453c82fatagen2026-09-24 13:07 UTC425
    emote: bool,
31ce2453c82fatagen2026-09-24 13:07 UTC426
) -> RoomMessageEventContentWithoutRelation {
31ce2453c82fatagen2026-09-24 13:07 UTC427
    let content = match content.formatted_body {
31ce2453c82fatagen2026-09-24 13:07 UTC428
        Some(formatted_body) if emote => {
31ce2453c82fatagen2026-09-24 13:07 UTC429
            RoomMessageEventContentWithoutRelation::emote_html(content.body, formatted_body)
31ce2453c82fatagen2026-09-24 13:07 UTC430
        }
31ce2453c82fatagen2026-09-24 13:07 UTC431
        Some(formatted_body) => {
31ce2453c82fatagen2026-09-24 13:07 UTC432
            RoomMessageEventContentWithoutRelation::text_html(content.body, formatted_body)
31ce2453c82fatagen2026-09-24 13:07 UTC433
        }
31ce2453c82fatagen2026-09-24 13:07 UTC434
        None if emote => RoomMessageEventContentWithoutRelation::emote_plain(content.body),
31ce2453c82fatagen2026-09-24 13:07 UTC435
        None => RoomMessageEventContentWithoutRelation::text_plain(content.body),
31ce2453c82fatagen2026-09-24 13:07 UTC436
    };
31ce2453c82fatagen2026-09-24 13:07 UTC437
    let content = content.add_mentions(Mentions::with_user_ids(mentions.user_ids));
31ce2453c82fatagen2026-09-24 13:07 UTC438
    if mentions.room {
31ce2453c82fatagen2026-09-24 13:07 UTC439
        content.add_mentions(Mentions::with_room_mention())
31ce2453c82fatagen2026-09-24 13:07 UTC440
    } else {
31ce2453c82fatagen2026-09-24 13:07 UTC441
        content
31ce2453c82fatagen2026-09-24 13:07 UTC442
    }
31ce2453c82fatagen2026-09-24 13:07 UTC443
}
31ce2453c82fatagen2026-09-24 13:07 UTC444
31ce2453c82fatagen2026-09-24 13:07 UTC445
pub(super) fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC446
    Error::with_source(ErrorKind::Timeline, "timeline operation failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC447
}