atagen

macltok

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

Blame: src/engine/timeline.rs

1fe480c16a3catagen2026-09-26 08:44 UTC1
use std::{collections::HashSet, future::Future, panic::AssertUnwindSafe, sync::Arc};
31ce2453c82fatagen2026-09-24 13:07 UTC2
31ce2453c82fatagen2026-09-24 13:07 UTC3
use futures_util::{FutureExt, StreamExt};
1fe480c16a3catagen2026-09-26 08:44 UTC4
use matrix_sdk::SendOutsideWasm;
31ce2453c82fatagen2026-09-24 13:07 UTC5
use matrix_sdk::{
31ce2453c82fatagen2026-09-24 13:07 UTC6
    room::edit::EditedContent,
31ce2453c82fatagen2026-09-24 13:07 UTC7
    room::reply::{EnforceThread, Reply as SdkReply},
31ce2453c82fatagen2026-09-24 13:07 UTC8
    ruma::{
31ce2453c82fatagen2026-09-24 13:07 UTC9
        api::client::receipt::create_receipt::v3::ReceiptType,
0827ad2d24e9atagen2026-10-23 06:49 UTC10
        events::room::message::{
0827ad2d24e9atagen2026-10-23 06:49 UTC11
            AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
31ce2453c82fatagen2026-09-24 13:07 UTC12
        },
31ce2453c82fatagen2026-09-24 13:07 UTC13
    },
31ce2453c82fatagen2026-09-24 13:07 UTC14
};
31ce2453c82fatagen2026-09-24 13:07 UTC15
use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};
31ce2453c82fatagen2026-09-24 13:07 UTC16
use ruma::{OwnedEventId, OwnedUserId};
d9b96952ec60atagen2026-10-25 09:07 UTC17
use tokio::sync::{mpsc, oneshot};
31ce2453c82fatagen2026-09-24 13:07 UTC18
1fe480c16a3catagen2026-09-26 08:44 UTC19
use super::executor::{EngineBoxFuture, JoinHandle, TaskSet, spawn};
31ce2453c82fatagen2026-09-24 13:07 UTC20
use crate::{
960ac0721079atagen2026-09-25 00:24 UTC21
    Error, ErrorKind, Event, EventRef, HistoryState, MessageContent, MessageEditContent,
960ac0721079atagen2026-09-25 00:24 UTC22
    MessageMentions, RichText, TimelineCommand, TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC23
    engine::MediaService,
31ce2453c82fatagen2026-09-24 13:07 UTC24
    normalise::{MediaScope, normalise_timeline, normalise_timeline_diffs},
31ce2453c82fatagen2026-09-24 13:07 UTC25
    transport::{EventSender, Responder, SendEventError},
0827ad2d24e9atagen2026-10-23 06:49 UTC26
    upload::sdk_mentions,
31ce2453c82fatagen2026-09-24 13:07 UTC27
};
31ce2453c82fatagen2026-09-24 13:07 UTC28
31ce2453c82fatagen2026-09-24 13:07 UTC29
const PAGINATION_QUEUE: usize = 32;
31ce2453c82fatagen2026-09-24 13:07 UTC30
31ce2453c82fatagen2026-09-24 13:07 UTC31
/// long-lived tasks inside a room actor report panics here so the actor can die loudly
31ce2453c82fatagen2026-09-24 13:07 UTC32
#[derive(Clone)]
31ce2453c82fatagen2026-09-24 13:07 UTC33
pub(super) struct Supervisor(mpsc::UnboundedSender<&'static str>);
31ce2453c82fatagen2026-09-24 13:07 UTC34
31ce2453c82fatagen2026-09-24 13:07 UTC35
impl Supervisor {
31ce2453c82fatagen2026-09-24 13:07 UTC36
    pub(super) fn new() -> (Self, mpsc::UnboundedReceiver<&'static str>) {
31ce2453c82fatagen2026-09-24 13:07 UTC37
        let (sender, receiver) = mpsc::unbounded_channel();
31ce2453c82fatagen2026-09-24 13:07 UTC38
        (Self(sender), receiver)
31ce2453c82fatagen2026-09-24 13:07 UTC39
    }
31ce2453c82fatagen2026-09-24 13:07 UTC40
31ce2453c82fatagen2026-09-24 13:07 UTC41
    pub(super) fn spawn(
31ce2453c82fatagen2026-09-24 13:07 UTC42
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC43
        label: &'static str,
1fe480c16a3catagen2026-09-26 08:44 UTC44
        task: impl Future<Output = ()> + SendOutsideWasm + 'static,
31ce2453c82fatagen2026-09-24 13:07 UTC45
    ) -> JoinHandle<()> {
31ce2453c82fatagen2026-09-24 13:07 UTC46
        let panics = self.0.clone();
1fe480c16a3catagen2026-09-26 08:44 UTC47
        spawn(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC48
            if AssertUnwindSafe(task).catch_unwind().await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC49
                let _ = panics.send(label);
31ce2453c82fatagen2026-09-24 13:07 UTC50
            }
31ce2453c82fatagen2026-09-24 13:07 UTC51
        })
31ce2453c82fatagen2026-09-24 13:07 UTC52
    }
31ce2453c82fatagen2026-09-24 13:07 UTC53
}
31ce2453c82fatagen2026-09-24 13:07 UTC54
5a52012c9b1datagen2026-09-24 23:45 UTC55
/// aborts its task when dropped, so an actor that dies mid-flight takes its helpers with it
5a52012c9b1datagen2026-09-24 23:45 UTC56
pub(super) struct TaskGuard(pub(super) JoinHandle<()>);
5a52012c9b1datagen2026-09-24 23:45 UTC57
5a52012c9b1datagen2026-09-24 23:45 UTC58
impl Drop for TaskGuard {
5a52012c9b1datagen2026-09-24 23:45 UTC59
    fn drop(&mut self) {
5a52012c9b1datagen2026-09-24 23:45 UTC60
        self.0.abort();
5a52012c9b1datagen2026-09-24 23:45 UTC61
    }
5a52012c9b1datagen2026-09-24 23:45 UTC62
}
5a52012c9b1datagen2026-09-24 23:45 UTC63
31ce2453c82fatagen2026-09-24 13:07 UTC64
/// what a timeline session needs from the room that owns it
31ce2453c82fatagen2026-09-24 13:07 UTC65
#[derive(Clone)]
31ce2453c82fatagen2026-09-24 13:07 UTC66
pub(super) struct SessionContext {
31ce2453c82fatagen2026-09-24 13:07 UTC67
    pub(super) events: EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC68
    pub(super) media: Arc<MediaService>,
31ce2453c82fatagen2026-09-24 13:07 UTC69
    pub(super) current_user_id: Option<OwnedUserId>,
31ce2453c82fatagen2026-09-24 13:07 UTC70
    pub(super) initial_history_limit: u16,
31ce2453c82fatagen2026-09-24 13:07 UTC71
    pub(super) supervisor: Supervisor,
31ce2453c82fatagen2026-09-24 13:07 UTC72
}
31ce2453c82fatagen2026-09-24 13:07 UTC73
31ce2453c82fatagen2026-09-24 13:07 UTC74
pub(super) struct TimelineSession {
31ce2453c82fatagen2026-09-24 13:07 UTC75
    pub(super) id: TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC76
    timeline: Arc<Timeline>,
31ce2453c82fatagen2026-09-24 13:07 UTC77
    context: SessionContext,
31ce2453c82fatagen2026-09-24 13:07 UTC78
    scope: MediaScope,
31ce2453c82fatagen2026-09-24 13:07 UTC79
    watcher: JoinHandle<()>,
d9b96952ec60atagen2026-10-25 09:07 UTC80
    fences: mpsc::UnboundedSender<Fence>,
31ce2453c82fatagen2026-09-24 13:07 UTC81
    pagination: mpsc::Sender<PaginationRequest>,
31ce2453c82fatagen2026-09-24 13:07 UTC82
    pagination_worker: JoinHandle<()>,
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC83
    member_fetch: JoinHandle<()>,
31ce2453c82fatagen2026-09-24 13:07 UTC84
}
31ce2453c82fatagen2026-09-24 13:07 UTC85
31ce2453c82fatagen2026-09-24 13:07 UTC86
struct PaginationRequest {
31ce2453c82fatagen2026-09-24 13:07 UTC87
    limit: u16,
31ce2453c82fatagen2026-09-24 13:07 UTC88
    reply: Option<Responder>,
31ce2453c82fatagen2026-09-24 13:07 UTC89
}
31ce2453c82fatagen2026-09-24 13:07 UTC90
31ce2453c82fatagen2026-09-24 13:07 UTC91
impl TimelineSession {
31ce2453c82fatagen2026-09-24 13:07 UTC92
    pub(super) async fn start(
31ce2453c82fatagen2026-09-24 13:07 UTC93
        id: TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC94
        timeline: Timeline,
31ce2453c82fatagen2026-09-24 13:07 UTC95
        context: SessionContext,
31ce2453c82fatagen2026-09-24 13:07 UTC96
    ) -> Result<Self, SendEventError> {
31ce2453c82fatagen2026-09-24 13:07 UTC97
        let timeline = Arc::new(timeline);
31ce2453c82fatagen2026-09-24 13:07 UTC98
        let scope = MediaScope::new();
d9b96952ec60atagen2026-10-25 09:07 UTC99
        let (watcher, fences) = start_watcher(&id, &timeline, &context, &scope).await?;
80381e06a573atagen2026-10-25 03:02 UTC100
        let (pagination, pagination_worker) = start_pagination_worker(&id, &timeline, &context);
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC101
        // under lazy-loaded members the sdk leaves sender profiles unavailable until someone
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC102
        // asks, and a /members fetched any other way never reaches the timeline's items
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC103
        let member_fetch = context.supervisor.spawn("member fetch", {
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC104
            let timeline = timeline.clone();
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC105
            async move { timeline.fetch_members().await }
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC106
        });
31ce2453c82fatagen2026-09-24 13:07 UTC107
        let session = Self {
31ce2453c82fatagen2026-09-24 13:07 UTC108
            id,
31ce2453c82fatagen2026-09-24 13:07 UTC109
            timeline,
31ce2453c82fatagen2026-09-24 13:07 UTC110
            scope,
31ce2453c82fatagen2026-09-24 13:07 UTC111
            watcher,
d9b96952ec60atagen2026-10-25 09:07 UTC112
            fences,
31ce2453c82fatagen2026-09-24 13:07 UTC113
            pagination,
31ce2453c82fatagen2026-09-24 13:07 UTC114
            pagination_worker,
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC115
            member_fetch,
31ce2453c82fatagen2026-09-24 13:07 UTC116
            context,
31ce2453c82fatagen2026-09-24 13:07 UTC117
        };
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC118
        if session.context.initial_history_limit > 0 && !matches!(session.id, TimelineId::Room(_)) {
31ce2453c82fatagen2026-09-24 13:07 UTC119
            session.paginate(session.context.initial_history_limit, None);
31ce2453c82fatagen2026-09-24 13:07 UTC120
        }
31ce2453c82fatagen2026-09-24 13:07 UTC121
        Ok(session)
31ce2453c82fatagen2026-09-24 13:07 UTC122
    }
31ce2453c82fatagen2026-09-24 13:07 UTC123
31ce2453c82fatagen2026-09-24 13:07 UTC124
    /// swaps in a fresh watcher. the old one is dead before the reset goes out,
31ce2453c82fatagen2026-09-24 13:07 UTC125
    /// so no stale diff can land after it
31ce2453c82fatagen2026-09-24 13:07 UTC126
    pub(super) async fn resync(&mut self) -> Result<(), Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC127
        self.watcher.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC128
        let _ = (&mut self.watcher).await;
d9b96952ec60atagen2026-10-25 09:07 UTC129
        (self.watcher, self.fences) =
d9b96952ec60atagen2026-10-25 09:07 UTC130
            start_watcher(&self.id, &self.timeline, &self.context, &self.scope)
d9b96952ec60atagen2026-10-25 09:07 UTC131
                .await
d9b96952ec60atagen2026-10-25 09:07 UTC132
                .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;
31ce2453c82fatagen2026-09-24 13:07 UTC133
        Ok(())
31ce2453c82fatagen2026-09-24 13:07 UTC134
    }
31ce2453c82fatagen2026-09-24 13:07 UTC135
d9b96952ec60atagen2026-10-25 09:07 UTC136
    /// returns once the watcher has passed on the history the room cache committed
d9b96952ec60atagen2026-10-25 09:07 UTC137
    pub(super) async fn publish_history(&self) -> Result<(), Error> {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC138
        self.timeline
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC139
            .flush_history()
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC140
            .await
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC141
            .map_err(timeline_error)?;
d9b96952ec60atagen2026-10-25 09:07 UTC142
        let (fence, drained) = oneshot::channel();
d9b96952ec60atagen2026-10-25 09:07 UTC143
        self.fences.send(fence).map_err(|_| Error::interrupted())?;
d9b96952ec60atagen2026-10-25 09:07 UTC144
        drained.await.map_err(|_| Error::interrupted())
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC145
    }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC146
002c79e4c6ecatagen2026-10-25 10:42 UTC147
    pub(super) async fn has_events(&self) -> bool {
002c79e4c6ecatagen2026-10-25 10:42 UTC148
        self.timeline
002c79e4c6ecatagen2026-10-25 10:42 UTC149
            .items()
002c79e4c6ecatagen2026-10-25 10:42 UTC150
            .await
002c79e4c6ecatagen2026-10-25 10:42 UTC151
            .iter()
002c79e4c6ecatagen2026-10-25 10:42 UTC152
            .any(|item| item.as_event().is_some())
002c79e4c6ecatagen2026-10-25 10:42 UTC153
    }
002c79e4c6ecatagen2026-10-25 10:42 UTC154
31ce2453c82fatagen2026-09-24 13:07 UTC155
    pub(super) fn paginate(&self, limit: u16, reply: Option<Responder>) {
31ce2453c82fatagen2026-09-24 13:07 UTC156
        if let Err(error) = self.pagination.try_send(PaginationRequest { limit, reply }) {
31ce2453c82fatagen2026-09-24 13:07 UTC157
            let (request, error) = match error {
31ce2453c82fatagen2026-09-24 13:07 UTC158
                mpsc::error::TrySendError::Full(request) => (
31ce2453c82fatagen2026-09-24 13:07 UTC159
                    request,
31ce2453c82fatagen2026-09-24 13:07 UTC160
                    Error::new(ErrorKind::QueueFull, "pagination queue is full"),
31ce2453c82fatagen2026-09-24 13:07 UTC161
                ),
31ce2453c82fatagen2026-09-24 13:07 UTC162
                mpsc::error::TrySendError::Closed(request) => (
31ce2453c82fatagen2026-09-24 13:07 UTC163
                    request,
31ce2453c82fatagen2026-09-24 13:07 UTC164
                    Error::new(ErrorKind::TimelineNotOpen, "timeline is closing"),
31ce2453c82fatagen2026-09-24 13:07 UTC165
                ),
31ce2453c82fatagen2026-09-24 13:07 UTC166
            };
31ce2453c82fatagen2026-09-24 13:07 UTC167
            if let Some(reply) = request.reply {
31ce2453c82fatagen2026-09-24 13:07 UTC168
                reply.done(Err(error));
31ce2453c82fatagen2026-09-24 13:07 UTC169
            }
31ce2453c82fatagen2026-09-24 13:07 UTC170
        }
31ce2453c82fatagen2026-09-24 13:07 UTC171
    }
31ce2453c82fatagen2026-09-24 13:07 UTC172
80381e06a573atagen2026-10-25 03:02 UTC173
    async fn stop_without_close(&mut self) {
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC174
        self.member_fetch.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC175
        self.pagination_worker.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC176
        self.watcher.abort();
80381e06a573atagen2026-10-25 03:02 UTC177
        let _ = (&mut self.member_fetch).await;
80381e06a573atagen2026-10-25 03:02 UTC178
        let _ = (&mut self.pagination_worker).await;
31ce2453c82fatagen2026-09-24 13:07 UTC179
        let _ = (&mut self.watcher).await;
5a52012c9b1datagen2026-09-24 23:45 UTC180
        // after the watcher, or a last diff could register media for a timeline that's gone
5a52012c9b1datagen2026-09-24 23:45 UTC181
        self.context.media.release(&self.scope).await;
80381e06a573atagen2026-10-25 03:02 UTC182
    }
80381e06a573atagen2026-10-25 03:02 UTC183
80381e06a573atagen2026-10-25 03:02 UTC184
    /// pending paginations answer `Interrupted` as their responders drop
80381e06a573atagen2026-10-25 03:02 UTC185
    pub(super) async fn close(mut self) {
80381e06a573atagen2026-10-25 03:02 UTC186
        self.stop_without_close().await;
0bfa3f46f0beatagen2026-09-26 09:12 UTC187
        let _ = self
0bfa3f46f0beatagen2026-09-26 09:12 UTC188
            .context
0bfa3f46f0beatagen2026-09-26 09:12 UTC189
            .events
0bfa3f46f0beatagen2026-09-26 09:12 UTC190
            .send(Event::TimelineClosed {
0bfa3f46f0beatagen2026-09-26 09:12 UTC191
                timeline_id: self.id.clone(),
0bfa3f46f0beatagen2026-09-26 09:12 UTC192
            })
0bfa3f46f0beatagen2026-09-26 09:12 UTC193
            .await;
31ce2453c82fatagen2026-09-24 13:07 UTC194
    }
31ce2453c82fatagen2026-09-24 13:07 UTC195
31ce2453c82fatagen2026-09-24 13:07 UTC196
    pub(super) async fn execute(
31ce2453c82fatagen2026-09-24 13:07 UTC197
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC198
        command: TimelineCommand,
31ce2453c82fatagen2026-09-24 13:07 UTC199
        reply: Responder,
1fe480c16a3catagen2026-09-26 08:44 UTC200
        work: &mut TaskSet<()>,
31ce2453c82fatagen2026-09-24 13:07 UTC201
    ) {
31ce2453c82fatagen2026-09-24 13:07 UTC202
        let timeline = self.timeline.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC203
        let thread_root = self.id.thread_root().cloned();
1fe480c16a3catagen2026-09-26 08:44 UTC204
        let background: EngineBoxFuture<'static, Result<(), Error>> = match command {
31ce2453c82fatagen2026-09-24 13:07 UTC205
            TimelineCommand::Paginate { limit, .. } => return self.paginate(limit, Some(reply)),
31ce2453c82fatagen2026-09-24 13:07 UTC206
            // network-bound, so it runs beside the actor instead of queueing the room behind it
960ac0721079atagen2026-09-25 00:24 UTC207
            TimelineCommand::Redact { target, reason, .. } => Box::pin(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC208
                timeline
960ac0721079atagen2026-09-25 00:24 UTC209
                    .redact(&item_id(target), reason.as_deref())
31ce2453c82fatagen2026-09-24 13:07 UTC210
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC211
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC212
            }),
8fa8a7cde89catagen2026-10-08 12:39 UTC213
            TimelineCommand::ToggleReaction {
8fa8a7cde89catagen2026-10-08 12:39 UTC214
                target,
8fa8a7cde89catagen2026-10-08 12:39 UTC215
                key,
8fa8a7cde89catagen2026-10-08 12:39 UTC216
                shortcode,
8fa8a7cde89catagen2026-10-08 12:39 UTC217
                ..
8fa8a7cde89catagen2026-10-08 12:39 UTC218
            } => Box::pin(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC219
                timeline
8fa8a7cde89catagen2026-10-08 12:39 UTC220
                    .toggle_reaction_with_shortcode(&item_id(target), &key, shortcode.as_deref())
31ce2453c82fatagen2026-09-24 13:07 UTC221
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC222
                    .map(drop)
31ce2453c82fatagen2026-09-24 13:07 UTC223
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC224
            }),
31ce2453c82fatagen2026-09-24 13:07 UTC225
            TimelineCommand::MarkRead { event_id, .. } => Box::pin(async move {
31ce2453c82fatagen2026-09-24 13:07 UTC226
                let receipt_failed =
31ce2453c82fatagen2026-09-24 13:07 UTC227
                    |source| Error::from_sdk("sending read receipt failed", source);
31ce2453c82fatagen2026-09-24 13:07 UTC228
                timeline
31ce2453c82fatagen2026-09-24 13:07 UTC229
                    .send_single_receipt(ReceiptType::Read, event_id.clone())
31ce2453c82fatagen2026-09-24 13:07 UTC230
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC231
                    .map_err(receipt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC232
                if thread_root.is_none() {
31ce2453c82fatagen2026-09-24 13:07 UTC233
                    timeline
31ce2453c82fatagen2026-09-24 13:07 UTC234
                        .send_single_receipt(ReceiptType::FullyRead, event_id)
31ce2453c82fatagen2026-09-24 13:07 UTC235
                        .await
31ce2453c82fatagen2026-09-24 13:07 UTC236
                        .map_err(receipt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC237
                }
31ce2453c82fatagen2026-09-24 13:07 UTC238
                Ok(())
31ce2453c82fatagen2026-09-24 13:07 UTC239
            }),
31ce2453c82fatagen2026-09-24 13:07 UTC240
            command => return reply.done(self.execute_serial(command).await),
31ce2453c82fatagen2026-09-24 13:07 UTC241
        };
31ce2453c82fatagen2026-09-24 13:07 UTC242
        work.spawn(async move { reply.done(background.await) });
31ce2453c82fatagen2026-09-24 13:07 UTC243
    }
31ce2453c82fatagen2026-09-24 13:07 UTC244
31ce2453c82fatagen2026-09-24 13:07 UTC245
    /// sends stay on the actor so they hit the send queue in the order they were asked for
31ce2453c82fatagen2026-09-24 13:07 UTC246
    async fn execute_serial(&self, command: TimelineCommand) -> Result<(), Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC247
        let timeline = &self.timeline;
31ce2453c82fatagen2026-09-24 13:07 UTC248
        match command {
31ce2453c82fatagen2026-09-24 13:07 UTC249
            TimelineCommand::Send { content, .. } => {
31ce2453c82fatagen2026-09-24 13:07 UTC250
                let body = match &content {
31ce2453c82fatagen2026-09-24 13:07 UTC251
                    MessageContent::Text { content, .. }
31ce2453c82fatagen2026-09-24 13:07 UTC252
                    | MessageContent::Emote { content, .. }
9bd34e25bb86atagen2026-10-24 07:23 UTC253
                    | MessageContent::Notice { content, .. }
31ce2453c82fatagen2026-09-24 13:07 UTC254
                    | MessageContent::Reply { content, .. } => &content.body,
31ce2453c82fatagen2026-09-24 13:07 UTC255
                };
31ce2453c82fatagen2026-09-24 13:07 UTC256
                if body.trim().is_empty() {
31ce2453c82fatagen2026-09-24 13:07 UTC257
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
31ce2453c82fatagen2026-09-24 13:07 UTC258
                }
31ce2453c82fatagen2026-09-24 13:07 UTC259
                let content = match content {
31ce2453c82fatagen2026-09-24 13:07 UTC260
                    MessageContent::Text { content, mentions } => {
9bd34e25bb86atagen2026-10-24 07:23 UTC261
                        message_content(content, mentions, Msgtype::Text).with_relation(None)
31ce2453c82fatagen2026-09-24 13:07 UTC262
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC263
                    MessageContent::Emote { content, mentions } => {
9bd34e25bb86atagen2026-10-24 07:23 UTC264
                        message_content(content, mentions, Msgtype::Emote).with_relation(None)
9bd34e25bb86atagen2026-10-24 07:23 UTC265
                    }
9bd34e25bb86atagen2026-10-24 07:23 UTC266
                    MessageContent::Notice { content, mentions } => {
9bd34e25bb86atagen2026-10-24 07:23 UTC267
                        message_content(content, mentions, Msgtype::Notice).with_relation(None)
31ce2453c82fatagen2026-09-24 13:07 UTC268
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC269
                    MessageContent::Reply {
31ce2453c82fatagen2026-09-24 13:07 UTC270
                        event_id,
31ce2453c82fatagen2026-09-24 13:07 UTC271
                        content,
31ce2453c82fatagen2026-09-24 13:07 UTC272
                        mentions,
31ce2453c82fatagen2026-09-24 13:07 UTC273
                        notify_replied_to,
31ce2453c82fatagen2026-09-24 13:07 UTC274
                    } => {
31ce2453c82fatagen2026-09-24 13:07 UTC275
                        self.reply_content(event_id, content, mentions, notify_replied_to)
31ce2453c82fatagen2026-09-24 13:07 UTC276
                            .await?
31ce2453c82fatagen2026-09-24 13:07 UTC277
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC278
                };
31ce2453c82fatagen2026-09-24 13:07 UTC279
                timeline
31ce2453c82fatagen2026-09-24 13:07 UTC280
                    .send(content.into())
31ce2453c82fatagen2026-09-24 13:07 UTC281
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC282
                    .map(drop)
31ce2453c82fatagen2026-09-24 13:07 UTC283
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC284
            }
31ce2453c82fatagen2026-09-24 13:07 UTC285
            TimelineCommand::Edit {
960ac0721079atagen2026-09-25 00:24 UTC286
                target, content, ..
31ce2453c82fatagen2026-09-24 13:07 UTC287
            } => {
9bd34e25bb86atagen2026-10-24 07:23 UTC288
                let (content, mentions, msgtype) = match content {
9bd34e25bb86atagen2026-10-24 07:23 UTC289
                    MessageEditContent::Text { content, mentions } => {
9bd34e25bb86atagen2026-10-24 07:23 UTC290
                        (content, mentions, Msgtype::Text)
9bd34e25bb86atagen2026-10-24 07:23 UTC291
                    }
9bd34e25bb86atagen2026-10-24 07:23 UTC292
                    MessageEditContent::Emote { content, mentions } => {
9bd34e25bb86atagen2026-10-24 07:23 UTC293
                        (content, mentions, Msgtype::Emote)
9bd34e25bb86atagen2026-10-24 07:23 UTC294
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC295
                };
31ce2453c82fatagen2026-09-24 13:07 UTC296
                if content.body.trim().is_empty() {
31ce2453c82fatagen2026-09-24 13:07 UTC297
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
31ce2453c82fatagen2026-09-24 13:07 UTC298
                }
31ce2453c82fatagen2026-09-24 13:07 UTC299
                timeline
31ce2453c82fatagen2026-09-24 13:07 UTC300
                    .edit(
960ac0721079atagen2026-09-25 00:24 UTC301
                        &item_id(target),
9bd34e25bb86atagen2026-10-24 07:23 UTC302
                        EditedContent::RoomMessage(message_content(content, mentions, msgtype)),
31ce2453c82fatagen2026-09-24 13:07 UTC303
                    )
31ce2453c82fatagen2026-09-24 13:07 UTC304
                    .await
31ce2453c82fatagen2026-09-24 13:07 UTC305
                    .map_err(timeline_error)
31ce2453c82fatagen2026-09-24 13:07 UTC306
            }
31ce2453c82fatagen2026-09-24 13:07 UTC307
            _ => Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC308
                ErrorKind::Other,
31ce2453c82fatagen2026-09-24 13:07 UTC309
                "timeline command was misrouted",
31ce2453c82fatagen2026-09-24 13:07 UTC310
            )),
31ce2453c82fatagen2026-09-24 13:07 UTC311
        }
31ce2453c82fatagen2026-09-24 13:07 UTC312
    }
31ce2453c82fatagen2026-09-24 13:07 UTC313
31ce2453c82fatagen2026-09-24 13:07 UTC314
    async fn reply_content(
31ce2453c82fatagen2026-09-24 13:07 UTC315
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC316
        event_id: OwnedEventId,
31ce2453c82fatagen2026-09-24 13:07 UTC317
        content: RichText,
31ce2453c82fatagen2026-09-24 13:07 UTC318
        mentions: MessageMentions,
31ce2453c82fatagen2026-09-24 13:07 UTC319
        notify_replied_to: bool,
31ce2453c82fatagen2026-09-24 13:07 UTC320
    ) -> Result<matrix_sdk::ruma::events::room::message::RoomMessageEventContent, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC321
        let thread_root = self.id.thread_root();
31ce2453c82fatagen2026-09-24 13:07 UTC322
        let content = self
31ce2453c82fatagen2026-09-24 13:07 UTC323
            .timeline
31ce2453c82fatagen2026-09-24 13:07 UTC324
            .room()
31ce2453c82fatagen2026-09-24 13:07 UTC325
            .make_reply_event(
9bd34e25bb86atagen2026-10-24 07:23 UTC326
                message_content(content, mentions, Msgtype::Text),
31ce2453c82fatagen2026-09-24 13:07 UTC327
                SdkReply {
31ce2453c82fatagen2026-09-24 13:07 UTC328
                    event_id,
31ce2453c82fatagen2026-09-24 13:07 UTC329
                    enforce_thread: if thread_root.is_some() {
31ce2453c82fatagen2026-09-24 13:07 UTC330
                        EnforceThread::Threaded(ReplyWithinThread::Yes)
31ce2453c82fatagen2026-09-24 13:07 UTC331
                    } else {
31ce2453c82fatagen2026-09-24 13:07 UTC332
                        EnforceThread::MaybeThreaded
31ce2453c82fatagen2026-09-24 13:07 UTC333
                    },
31ce2453c82fatagen2026-09-24 13:07 UTC334
                    add_mentions: if notify_replied_to {
31ce2453c82fatagen2026-09-24 13:07 UTC335
                        AddMentions::Yes
31ce2453c82fatagen2026-09-24 13:07 UTC336
                    } else {
31ce2453c82fatagen2026-09-24 13:07 UTC337
                        AddMentions::No
31ce2453c82fatagen2026-09-24 13:07 UTC338
                    },
31ce2453c82fatagen2026-09-24 13:07 UTC339
                },
31ce2453c82fatagen2026-09-24 13:07 UTC340
            )
31ce2453c82fatagen2026-09-24 13:07 UTC341
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC342
            .map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC343
                Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC344
            })?;
31ce2453c82fatagen2026-09-24 13:07 UTC345
        if let Some(thread_root) = thread_root
31ce2453c82fatagen2026-09-24 13:07 UTC346
            && !matches!(
31ce2453c82fatagen2026-09-24 13:07 UTC347
                content.relates_to.as_ref(),
31ce2453c82fatagen2026-09-24 13:07 UTC348
                Some(Relation::Thread(thread)) if &thread.event_id == thread_root
31ce2453c82fatagen2026-09-24 13:07 UTC349
            )
31ce2453c82fatagen2026-09-24 13:07 UTC350
        {
31ce2453c82fatagen2026-09-24 13:07 UTC351
            return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC352
                ErrorKind::Invalid,
31ce2453c82fatagen2026-09-24 13:07 UTC353
                "reply target does not belong to the selected thread",
31ce2453c82fatagen2026-09-24 13:07 UTC354
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC355
        }
31ce2453c82fatagen2026-09-24 13:07 UTC356
        Ok(content)
31ce2453c82fatagen2026-09-24 13:07 UTC357
    }
31ce2453c82fatagen2026-09-24 13:07 UTC358
}
31ce2453c82fatagen2026-09-24 13:07 UTC359
80381e06a573atagen2026-10-25 03:02 UTC360
fn start_pagination_worker(
80381e06a573atagen2026-10-25 03:02 UTC361
    id: &TimelineId,
80381e06a573atagen2026-10-25 03:02 UTC362
    timeline: &Arc<Timeline>,
80381e06a573atagen2026-10-25 03:02 UTC363
    context: &SessionContext,
80381e06a573atagen2026-10-25 03:02 UTC364
) -> (mpsc::Sender<PaginationRequest>, JoinHandle<()>) {
80381e06a573atagen2026-10-25 03:02 UTC365
    let (pagination, requests) = mpsc::channel(PAGINATION_QUEUE);
80381e06a573atagen2026-10-25 03:02 UTC366
    let worker = context.supervisor.spawn(
80381e06a573atagen2026-10-25 03:02 UTC367
        "pagination worker",
80381e06a573atagen2026-10-25 03:02 UTC368
        run_pagination(
80381e06a573atagen2026-10-25 03:02 UTC369
            requests,
80381e06a573atagen2026-10-25 03:02 UTC370
            timeline.clone(),
80381e06a573atagen2026-10-25 03:02 UTC371
            context.events.clone(),
80381e06a573atagen2026-10-25 03:02 UTC372
            id.clone(),
80381e06a573atagen2026-10-25 03:02 UTC373
        ),
80381e06a573atagen2026-10-25 03:02 UTC374
    );
80381e06a573atagen2026-10-25 03:02 UTC375
    (pagination, worker)
80381e06a573atagen2026-10-25 03:02 UTC376
}
80381e06a573atagen2026-10-25 03:02 UTC377
31ce2453c82fatagen2026-09-24 13:07 UTC378
impl Drop for TimelineSession {
31ce2453c82fatagen2026-09-24 13:07 UTC379
    fn drop(&mut self) {
31ce2453c82fatagen2026-09-24 13:07 UTC380
        self.watcher.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC381
        self.pagination_worker.abort();
6ed8c0ef3a2aatagen2026-10-24 10:13 UTC382
        self.member_fetch.abort();
31ce2453c82fatagen2026-09-24 13:07 UTC383
    }
31ce2453c82fatagen2026-09-24 13:07 UTC384
}
31ce2453c82fatagen2026-09-24 13:07 UTC385
960ac0721079atagen2026-09-25 00:24 UTC386
fn item_id(target: EventRef) -> TimelineEventItemId {
960ac0721079atagen2026-09-25 00:24 UTC387
    match target {
960ac0721079atagen2026-09-25 00:24 UTC388
        EventRef::Remote(event_id) => TimelineEventItemId::EventId(event_id),
960ac0721079atagen2026-09-25 00:24 UTC389
        EventRef::Local(transaction_id) => TimelineEventItemId::TransactionId(transaction_id),
960ac0721079atagen2026-09-25 00:24 UTC390
    }
960ac0721079atagen2026-09-25 00:24 UTC391
}
960ac0721079atagen2026-09-25 00:24 UTC392
960ac0721079atagen2026-09-25 00:24 UTC393
/// the sdk leaves a reply's target unloaded until someone asks, so the watcher asks, once per event
960ac0721079atagen2026-09-25 00:24 UTC394
struct ReplyFetcher {
960ac0721079atagen2026-09-25 00:24 UTC395
    timeline: Arc<Timeline>,
960ac0721079atagen2026-09-25 00:24 UTC396
    asked: HashSet<OwnedEventId>,
1fe480c16a3catagen2026-09-26 08:44 UTC397
    fetches: TaskSet<()>,
960ac0721079atagen2026-09-25 00:24 UTC398
}
960ac0721079atagen2026-09-25 00:24 UTC399
960ac0721079atagen2026-09-25 00:24 UTC400
impl ReplyFetcher {
960ac0721079atagen2026-09-25 00:24 UTC401
    fn fetch(&mut self, missing: Vec<OwnedEventId>) {
960ac0721079atagen2026-09-25 00:24 UTC402
        for event_id in missing {
960ac0721079atagen2026-09-25 00:24 UTC403
            if self.asked.insert(event_id.clone()) {
960ac0721079atagen2026-09-25 00:24 UTC404
                let timeline = self.timeline.clone();
960ac0721079atagen2026-09-25 00:24 UTC405
                self.fetches.spawn(async move {
960ac0721079atagen2026-09-25 00:24 UTC406
                    if let Err(error) = timeline.fetch_details_for_event(&event_id).await {
960ac0721079atagen2026-09-25 00:24 UTC407
                        tracing::debug!(%event_id, %error, "fetching reply details failed");
960ac0721079atagen2026-09-25 00:24 UTC408
                    }
960ac0721079atagen2026-09-25 00:24 UTC409
                });
960ac0721079atagen2026-09-25 00:24 UTC410
            }
960ac0721079atagen2026-09-25 00:24 UTC411
        }
960ac0721079atagen2026-09-25 00:24 UTC412
    }
960ac0721079atagen2026-09-25 00:24 UTC413
}
960ac0721079atagen2026-09-25 00:24 UTC414
d9b96952ec60atagen2026-10-25 09:07 UTC415
/// asks the watcher to pass on every diff the timeline already holds, then answers
d9b96952ec60atagen2026-10-25 09:07 UTC416
type Fence = oneshot::Sender<()>;
d9b96952ec60atagen2026-10-25 09:07 UTC417
31ce2453c82fatagen2026-09-24 13:07 UTC418
async fn start_watcher(
31ce2453c82fatagen2026-09-24 13:07 UTC419
    id: &TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC420
    timeline: &Arc<Timeline>,
31ce2453c82fatagen2026-09-24 13:07 UTC421
    context: &SessionContext,
31ce2453c82fatagen2026-09-24 13:07 UTC422
    scope: &MediaScope,
d9b96952ec60atagen2026-10-25 09:07 UTC423
) -> Result<(JoinHandle<()>, mpsc::UnboundedSender<Fence>), SendEventError> {
31ce2453c82fatagen2026-09-24 13:07 UTC424
    let (items, mut stream) = timeline.subscribe().await;
31ce2453c82fatagen2026-09-24 13:07 UTC425
    let current_user_id = context.current_user_id.clone();
002c79e4c6ecatagen2026-10-25 10:42 UTC426
    let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), scope);
da8896938cf0atagen2026-10-25 10:04 UTC427
    context.media.register(scope, normalised.media).await;
31ce2453c82fatagen2026-09-24 13:07 UTC428
    context
31ce2453c82fatagen2026-09-24 13:07 UTC429
        .events
31ce2453c82fatagen2026-09-24 13:07 UTC430
        .send(Event::TimelineReset {
31ce2453c82fatagen2026-09-24 13:07 UTC431
            timeline_id: id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC432
            items: normalised.items,
31ce2453c82fatagen2026-09-24 13:07 UTC433
        })
31ce2453c82fatagen2026-09-24 13:07 UTC434
        .await?;
960ac0721079atagen2026-09-25 00:24 UTC435
    let mut replies = ReplyFetcher {
960ac0721079atagen2026-09-25 00:24 UTC436
        timeline: timeline.clone(),
960ac0721079atagen2026-09-25 00:24 UTC437
        asked: HashSet::new(),
1fe480c16a3catagen2026-09-26 08:44 UTC438
        fetches: TaskSet::new(),
960ac0721079atagen2026-09-25 00:24 UTC439
    };
960ac0721079atagen2026-09-25 00:24 UTC440
    replies.fetch(normalised.missing_replies);
d9b96952ec60atagen2026-10-25 09:07 UTC441
    let (fences, mut fence_requests) = mpsc::unbounded_channel::<Fence>();
31ce2453c82fatagen2026-09-24 13:07 UTC442
    let id = id.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC443
    let events = context.events.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC444
    let media = context.media.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC445
    let scope = scope.clone();
d9b96952ec60atagen2026-10-25 09:07 UTC446
    let task = context.supervisor.spawn("timeline watcher", async move {
960ac0721079atagen2026-09-25 00:24 UTC447
        loop {
d9b96952ec60atagen2026-10-25 09:07 UTC448
            let (diffs, fence) = tokio::select! {
960ac0721079atagen2026-09-25 00:24 UTC449
                diffs = stream.next() => match diffs {
d9b96952ec60atagen2026-10-25 09:07 UTC450
                    Some(diffs) => (vec![diffs], None),
960ac0721079atagen2026-09-25 00:24 UTC451
                    None => return,
960ac0721079atagen2026-09-25 00:24 UTC452
                },
d9b96952ec60atagen2026-10-25 09:07 UTC453
                Some(fence) = fence_requests.recv() => {
d9b96952ec60atagen2026-10-25 09:07 UTC454
                    let mut ready = Vec::new();
d9b96952ec60atagen2026-10-25 09:07 UTC455
                    while let Some(diffs) = stream.next().now_or_never() {
d9b96952ec60atagen2026-10-25 09:07 UTC456
                        match diffs {
d9b96952ec60atagen2026-10-25 09:07 UTC457
                            Some(diffs) => ready.push(diffs),
d9b96952ec60atagen2026-10-25 09:07 UTC458
                            None => return,
d9b96952ec60atagen2026-10-25 09:07 UTC459
                        }
d9b96952ec60atagen2026-10-25 09:07 UTC460
                    }
d9b96952ec60atagen2026-10-25 09:07 UTC461
                    (ready, Some(fence))
d9b96952ec60atagen2026-10-25 09:07 UTC462
                }
960ac0721079atagen2026-09-25 00:24 UTC463
                Some(_) = replies.fetches.join_next(), if !replies.fetches.is_empty() => continue,
960ac0721079atagen2026-09-25 00:24 UTC464
            };
d9b96952ec60atagen2026-10-25 09:07 UTC465
            for diffs in diffs {
002c79e4c6ecatagen2026-10-25 10:42 UTC466
                let normalised =
002c79e4c6ecatagen2026-10-25 10:42 UTC467
                    normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);
da8896938cf0atagen2026-10-25 10:04 UTC468
                media.register(&scope, normalised.media).await;
d9b96952ec60atagen2026-10-25 09:07 UTC469
                replies.fetch(normalised.missing_replies);
d9b96952ec60atagen2026-10-25 09:07 UTC470
                if normalised.diffs.is_empty() {
d9b96952ec60atagen2026-10-25 09:07 UTC471
                    continue;
d9b96952ec60atagen2026-10-25 09:07 UTC472
                }
d9b96952ec60atagen2026-10-25 09:07 UTC473
                let event = Event::TimelineChanged {
d9b96952ec60atagen2026-10-25 09:07 UTC474
                    timeline_id: id.clone(),
d9b96952ec60atagen2026-10-25 09:07 UTC475
                    diffs: normalised.diffs,
d9b96952ec60atagen2026-10-25 09:07 UTC476
                };
d9b96952ec60atagen2026-10-25 09:07 UTC477
                if events.send(event).await.is_err() {
d9b96952ec60atagen2026-10-25 09:07 UTC478
                    return;
d9b96952ec60atagen2026-10-25 09:07 UTC479
                }
d9b96952ec60atagen2026-10-25 09:07 UTC480
            }
d9b96952ec60atagen2026-10-25 09:07 UTC481
            if let Some(fence) = fence {
d9b96952ec60atagen2026-10-25 09:07 UTC482
                let _ = fence.send(());
31ce2453c82fatagen2026-09-24 13:07 UTC483
            }
31ce2453c82fatagen2026-09-24 13:07 UTC484
        }
d9b96952ec60atagen2026-10-25 09:07 UTC485
    });
d9b96952ec60atagen2026-10-25 09:07 UTC486
    Ok((task, fences))
31ce2453c82fatagen2026-09-24 13:07 UTC487
}
31ce2453c82fatagen2026-09-24 13:07 UTC488
31ce2453c82fatagen2026-09-24 13:07 UTC489
async fn run_pagination(
31ce2453c82fatagen2026-09-24 13:07 UTC490
    mut requests: mpsc::Receiver<PaginationRequest>,
31ce2453c82fatagen2026-09-24 13:07 UTC491
    timeline: Arc<Timeline>,
31ce2453c82fatagen2026-09-24 13:07 UTC492
    events: EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC493
    timeline_id: TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC494
) {
31ce2453c82fatagen2026-09-24 13:07 UTC495
    while let Some(PaginationRequest { limit, reply }) = requests.recv().await {
31ce2453c82fatagen2026-09-24 13:07 UTC496
        let history = |state| Event::HistoryChanged {
31ce2453c82fatagen2026-09-24 13:07 UTC497
            timeline_id: timeline_id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC498
            state,
31ce2453c82fatagen2026-09-24 13:07 UTC499
        };
31ce2453c82fatagen2026-09-24 13:07 UTC500
        if events.send(history(HistoryState::Loading)).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC501
            return;
31ce2453c82fatagen2026-09-24 13:07 UTC502
        }
2819830aef32atagen2026-10-26 08:42 UTC503
        tracing::debug!(?timeline_id, limit, "paginating backwards");
31ce2453c82fatagen2026-09-24 13:07 UTC504
        let result = timeline
31ce2453c82fatagen2026-09-24 13:07 UTC505
            .paginate_backwards(limit.max(1))
31ce2453c82fatagen2026-09-24 13:07 UTC506
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC507
            .map_err(timeline_error);
2819830aef32atagen2026-10-26 08:42 UTC508
        tracing::debug!(?timeline_id, ?result, "paginated backwards");
31ce2453c82fatagen2026-09-24 13:07 UTC509
        let state = match &result {
31ce2453c82fatagen2026-09-24 13:07 UTC510
            Ok(true) => HistoryState::StartReached,
31ce2453c82fatagen2026-09-24 13:07 UTC511
            Ok(false) => HistoryState::Idle,
31ce2453c82fatagen2026-09-24 13:07 UTC512
            Err(error) => HistoryState::Failed(error.to_string()),
31ce2453c82fatagen2026-09-24 13:07 UTC513
        };
31ce2453c82fatagen2026-09-24 13:07 UTC514
        let _ = events.send(history(state)).await;
31ce2453c82fatagen2026-09-24 13:07 UTC515
        if let Some(reply) = reply {
31ce2453c82fatagen2026-09-24 13:07 UTC516
            reply.done(result.map(drop));
31ce2453c82fatagen2026-09-24 13:07 UTC517
        }
31ce2453c82fatagen2026-09-24 13:07 UTC518
    }
31ce2453c82fatagen2026-09-24 13:07 UTC519
}
31ce2453c82fatagen2026-09-24 13:07 UTC520
9bd34e25bb86atagen2026-10-24 07:23 UTC521
#[derive(Clone, Copy)]
9bd34e25bb86atagen2026-10-24 07:23 UTC522
enum Msgtype {
9bd34e25bb86atagen2026-10-24 07:23 UTC523
    Text,
9bd34e25bb86atagen2026-10-24 07:23 UTC524
    Emote,
9bd34e25bb86atagen2026-10-24 07:23 UTC525
    Notice,
9bd34e25bb86atagen2026-10-24 07:23 UTC526
}
9bd34e25bb86atagen2026-10-24 07:23 UTC527
31ce2453c82fatagen2026-09-24 13:07 UTC528
fn message_content(
31ce2453c82fatagen2026-09-24 13:07 UTC529
    content: RichText,
31ce2453c82fatagen2026-09-24 13:07 UTC530
    mentions: MessageMentions,
9bd34e25bb86atagen2026-10-24 07:23 UTC531
    msgtype: Msgtype,
31ce2453c82fatagen2026-09-24 13:07 UTC532
) -> RoomMessageEventContentWithoutRelation {
9bd34e25bb86atagen2026-10-24 07:23 UTC533
    type Content = RoomMessageEventContentWithoutRelation;
9bd34e25bb86atagen2026-10-24 07:23 UTC534
    let (body, formatted_body) = (content.body, content.formatted_body);
9bd34e25bb86atagen2026-10-24 07:23 UTC535
    let content = match (msgtype, formatted_body) {
9bd34e25bb86atagen2026-10-24 07:23 UTC536
        (Msgtype::Text, Some(html)) => Content::text_html(body, html),
9bd34e25bb86atagen2026-10-24 07:23 UTC537
        (Msgtype::Text, None) => Content::text_plain(body),
9bd34e25bb86atagen2026-10-24 07:23 UTC538
        (Msgtype::Emote, Some(html)) => Content::emote_html(body, html),
9bd34e25bb86atagen2026-10-24 07:23 UTC539
        (Msgtype::Emote, None) => Content::emote_plain(body),
9bd34e25bb86atagen2026-10-24 07:23 UTC540
        (Msgtype::Notice, Some(html)) => Content::notice_html(body, html),
9bd34e25bb86atagen2026-10-24 07:23 UTC541
        (Msgtype::Notice, None) => Content::notice_plain(body),
31ce2453c82fatagen2026-09-24 13:07 UTC542
    };
0827ad2d24e9atagen2026-10-23 06:49 UTC543
    content.add_mentions(sdk_mentions(mentions))
31ce2453c82fatagen2026-09-24 13:07 UTC544
}
31ce2453c82fatagen2026-09-24 13:07 UTC545
31ce2453c82fatagen2026-09-24 13:07 UTC546
pub(super) fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC547
    Error::with_source(ErrorKind::Timeline, "timeline operation failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC548
}