atagen

macltok

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

Blame: src/normalise/timeline.rs

96fdf90aacd6atagen2026-09-08 08:24 UTC1
use std::sync::Arc;
96fdf90aacd6atagen2026-09-08 08:24 UTC2
960ac0721079atagen2026-09-25 00:24 UTC3
use matrix_sdk_crypto::types::events::UtdCause as SdkUtdCause;
96fdf90aacd6atagen2026-09-08 08:24 UTC4
use matrix_sdk_ui::{
96fdf90aacd6atagen2026-09-08 08:24 UTC5
    eyeball_im::VectorDiff,
96fdf90aacd6atagen2026-09-08 08:24 UTC6
    timeline::{
960ac0721079atagen2026-09-25 00:24 UTC7
        AnyOtherStateEventContentChange, EncryptedMessage, EventSendState, EventTimelineItem,
960ac0721079atagen2026-09-25 00:24 UTC8
        MembershipChange as SdkMembershipChange, MsgLikeContent, MsgLikeKind, Profile,
960ac0721079atagen2026-09-25 00:24 UTC9
        TimelineDetails, TimelineEventItemId, TimelineItem as SdkTimelineItem, TimelineItemContent,
960ac0721079atagen2026-09-25 00:24 UTC10
        TimelineItemKind, VirtualTimelineItem,
96fdf90aacd6atagen2026-09-08 08:24 UTC11
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC12
};
96fdf90aacd6atagen2026-09-08 08:24 UTC13
use ruma::{
8fa8a7cde89catagen2026-10-08 12:39 UTC14
    MxcUri, OwnedEventId, UserId,
960ac0721079atagen2026-09-25 00:24 UTC15
    events::{
960ac0721079atagen2026-09-25 00:24 UTC16
        StateEventContentChange,
960ac0721079atagen2026-09-25 00:24 UTC17
        room::message::{MessageFormat, MessageType},
960ac0721079atagen2026-09-25 00:24 UTC18
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC19
};
96fdf90aacd6atagen2026-09-08 08:24 UTC20
96fdf90aacd6atagen2026-09-08 08:24 UTC21
use crate::{
d9b96952ec60atagen2026-10-25 09:07 UTC22
    Change, HistoryGap, HistoryGapLifecycle, MediaId, MembershipChange, Message, MessageKind,
d9b96952ec60atagen2026-10-25 09:07 UTC23
    MessageMentions, Reaction, ReplyDetails, ReplyReference, SendState, Sender, ThreadSummary,
d9b96952ec60atagen2026-10-25 09:07 UTC24
    TimelineContent, TimelineDiff, TimelineEvent, TimelineItem, TimelineItemId, UtdCause,
96fdf90aacd6atagen2026-09-08 08:24 UTC25
};
96fdf90aacd6atagen2026-09-08 08:24 UTC26
0291516d40b5atagen2026-10-22 08:31 UTC27
use super::link_preview::sender_previews;
96fdf90aacd6atagen2026-09-08 08:24 UTC28
use super::media::{
96fdf90aacd6atagen2026-09-08 08:24 UTC29
    MediaRegistration, MediaScope, media_descriptor_for_sticker, media_from_message,
0291516d40b5atagen2026-10-22 08:31 UTC30
    media_identity, media_registrations_for_item,
96fdf90aacd6atagen2026-09-08 08:24 UTC31
};
96fdf90aacd6atagen2026-09-08 08:24 UTC32
960ac0721079atagen2026-09-25 00:24 UTC33
#[derive(Default)]
96fdf90aacd6atagen2026-09-08 08:24 UTC34
pub(crate) struct NormalisedTimeline {
d5d8097c3afdatagen2026-09-26 09:33 UTC35
    pub(crate) items: imbl::Vector<TimelineItem>,
96fdf90aacd6atagen2026-09-08 08:24 UTC36
    pub(crate) media: Vec<MediaRegistration>,
960ac0721079atagen2026-09-25 00:24 UTC37
    /// replies whose target isn't loaded, so somebody should go and ask for it
960ac0721079atagen2026-09-25 00:24 UTC38
    pub(crate) missing_replies: Vec<OwnedEventId>,
960ac0721079atagen2026-09-25 00:24 UTC39
}
960ac0721079atagen2026-09-25 00:24 UTC40
960ac0721079atagen2026-09-25 00:24 UTC41
pub(crate) struct NormalisedTimelineDiffs {
960ac0721079atagen2026-09-25 00:24 UTC42
    pub(crate) diffs: Vec<TimelineDiff>,
960ac0721079atagen2026-09-25 00:24 UTC43
    pub(crate) media: Vec<MediaRegistration>,
960ac0721079atagen2026-09-25 00:24 UTC44
    pub(crate) missing_replies: Vec<OwnedEventId>,
960ac0721079atagen2026-09-25 00:24 UTC45
}
960ac0721079atagen2026-09-25 00:24 UTC46
960ac0721079atagen2026-09-25 00:24 UTC47
struct Normaliser<'a> {
960ac0721079atagen2026-09-25 00:24 UTC48
    current_user_id: Option<&'a UserId>,
960ac0721079atagen2026-09-25 00:24 UTC49
    scope: &'a MediaScope,
960ac0721079atagen2026-09-25 00:24 UTC50
    media: Vec<MediaRegistration>,
960ac0721079atagen2026-09-25 00:24 UTC51
    missing_replies: Vec<OwnedEventId>,
960ac0721079atagen2026-09-25 00:24 UTC52
}
960ac0721079atagen2026-09-25 00:24 UTC53
960ac0721079atagen2026-09-25 00:24 UTC54
impl Normaliser<'_> {
d9b96952ec60atagen2026-10-25 09:07 UTC55
    /// `None` for rows macltok leaves out
d9b96952ec60atagen2026-10-25 09:07 UTC56
    fn item(&mut self, item: &SdkTimelineItem) -> Option<TimelineItem> {
d9b96952ec60atagen2026-10-25 09:07 UTC57
        if let TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(gap)) = item.kind()
d9b96952ec60atagen2026-10-25 09:07 UTC58
            && gap.status == matrix_sdk::event_cache::HistoryGapStatus::Joined
d9b96952ec60atagen2026-10-25 09:07 UTC59
        {
d9b96952ec60atagen2026-10-25 09:07 UTC60
            return None;
d9b96952ec60atagen2026-10-25 09:07 UTC61
        }
0291516d40b5atagen2026-10-22 08:31 UTC62
        self.media
0291516d40b5atagen2026-10-22 08:31 UTC63
            .extend(media_registrations_for_item(item, self.scope));
d9b96952ec60atagen2026-10-25 09:07 UTC64
        Some(match item.kind() {
960ac0721079atagen2026-09-25 00:24 UTC65
            TimelineItemKind::Event(event) => {
960ac0721079atagen2026-09-25 00:24 UTC66
                TimelineItem::Event(Box::new(self.event(item, event)))
960ac0721079atagen2026-09-25 00:24 UTC67
            }
960ac0721079atagen2026-09-25 00:24 UTC68
            TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => {
960ac0721079atagen2026-09-25 00:24 UTC69
                TimelineItem::DayDivider {
960ac0721079atagen2026-09-25 00:24 UTC70
                    timestamp_ms: timestamp.get().into(),
960ac0721079atagen2026-09-25 00:24 UTC71
                }
960ac0721079atagen2026-09-25 00:24 UTC72
            }
960ac0721079atagen2026-09-25 00:24 UTC73
            TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => TimelineItem::ReadMarker,
960ac0721079atagen2026-09-25 00:24 UTC74
            TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {
960ac0721079atagen2026-09-25 00:24 UTC75
                TimelineItem::TimelineStart
960ac0721079atagen2026-09-25 00:24 UTC76
            }
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC77
            TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(gap)) => {
d9b96952ec60atagen2026-10-25 09:07 UTC78
                use matrix_sdk::event_cache::HistoryGapStatus;
d9b96952ec60atagen2026-10-25 09:07 UTC79
                let (lifecycle, failure) = match &gap.status {
d9b96952ec60atagen2026-10-25 09:07 UTC80
                    HistoryGapStatus::Idle | HistoryGapStatus::Joined => {
d9b96952ec60atagen2026-10-25 09:07 UTC81
                        (HistoryGapLifecycle::Idle, None)
d9b96952ec60atagen2026-10-25 09:07 UTC82
                    }
d9b96952ec60atagen2026-10-25 09:07 UTC83
                    HistoryGapStatus::Paused => (HistoryGapLifecycle::Paused, None),
d9b96952ec60atagen2026-10-25 09:07 UTC84
                    HistoryGapStatus::Failed { message } => {
d9b96952ec60atagen2026-10-25 09:07 UTC85
                        (HistoryGapLifecycle::Failed, Some(message.clone()))
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC86
                    }
d9b96952ec60atagen2026-10-25 09:07 UTC87
                    HistoryGapStatus::Unavailable => (HistoryGapLifecycle::Unavailable, None),
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC88
                };
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC89
                TimelineItem::HistoryGap(HistoryGap {
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC90
                    id: gap.id.clone(),
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC91
                    older_boundary: gap.older_boundary.clone(),
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC92
                    newer_boundary: gap.newer_boundary.clone(),
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC93
                    lifecycle,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC94
                    failure,
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC95
                })
c9f0fd0ee1ccatagen2026-10-25 06:16 UTC96
            }
d9b96952ec60atagen2026-10-25 09:07 UTC97
        })
960ac0721079atagen2026-09-25 00:24 UTC98
    }
960ac0721079atagen2026-09-25 00:24 UTC99
960ac0721079atagen2026-09-25 00:24 UTC100
    fn event(&mut self, item: &SdkTimelineItem, event: &EventTimelineItem) -> TimelineEvent {
960ac0721079atagen2026-09-25 00:24 UTC101
        let msg_like = match event.content() {
960ac0721079atagen2026-09-25 00:24 UTC102
            TimelineItemContent::MsgLike(content) => Some(content),
960ac0721079atagen2026-09-25 00:24 UTC103
            _ => None,
960ac0721079atagen2026-09-25 00:24 UTC104
        };
960ac0721079atagen2026-09-25 00:24 UTC105
        let reply_to = msg_like
960ac0721079atagen2026-09-25 00:24 UTC106
            .and_then(|content| content.in_reply_to.as_ref())
960ac0721079atagen2026-09-25 00:24 UTC107
            .map(|reply| {
960ac0721079atagen2026-09-25 00:24 UTC108
                let details = match &reply.event {
960ac0721079atagen2026-09-25 00:24 UTC109
                    TimelineDetails::Ready(embedded) => ReplyDetails::Ready {
960ac0721079atagen2026-09-25 00:24 UTC110
                        sender: sender(&embedded.sender, &embedded.sender_profile),
960ac0721079atagen2026-09-25 00:24 UTC111
                        content: Box::new(content(&embedded.content, None)),
960ac0721079atagen2026-09-25 00:24 UTC112
                        timestamp_ms: embedded.timestamp.get().into(),
960ac0721079atagen2026-09-25 00:24 UTC113
                    },
960ac0721079atagen2026-09-25 00:24 UTC114
                    TimelineDetails::Unavailable => {
960ac0721079atagen2026-09-25 00:24 UTC115
                        self.missing_replies.push(reply.event_id.clone());
960ac0721079atagen2026-09-25 00:24 UTC116
                        ReplyDetails::Pending
960ac0721079atagen2026-09-25 00:24 UTC117
                    }
960ac0721079atagen2026-09-25 00:24 UTC118
                    TimelineDetails::Pending => ReplyDetails::Pending,
960ac0721079atagen2026-09-25 00:24 UTC119
                    TimelineDetails::Error(_) => ReplyDetails::Unavailable,
960ac0721079atagen2026-09-25 00:24 UTC120
                };
960ac0721079atagen2026-09-25 00:24 UTC121
                ReplyReference {
960ac0721079atagen2026-09-25 00:24 UTC122
                    event_id: reply.event_id.clone(),
960ac0721079atagen2026-09-25 00:24 UTC123
                    details,
960ac0721079atagen2026-09-25 00:24 UTC124
                }
960ac0721079atagen2026-09-25 00:24 UTC125
            });
960ac0721079atagen2026-09-25 00:24 UTC126
        let mut read_by: Vec<_> = event
960ac0721079atagen2026-09-25 00:24 UTC127
            .read_receipts()
960ac0721079atagen2026-09-25 00:24 UTC128
            .keys()
960ac0721079atagen2026-09-25 00:24 UTC129
            .filter(|user_id| *user_id != event.sender())
960ac0721079atagen2026-09-25 00:24 UTC130
            .cloned()
960ac0721079atagen2026-09-25 00:24 UTC131
            .collect();
960ac0721079atagen2026-09-25 00:24 UTC132
        read_by.sort();
960ac0721079atagen2026-09-25 00:24 UTC133
        TimelineEvent {
960ac0721079atagen2026-09-25 00:24 UTC134
            item_id: TimelineItemId::new(item.unique_id().0.clone()),
960ac0721079atagen2026-09-25 00:24 UTC135
            event_id: event.event_id().map(ToOwned::to_owned),
960ac0721079atagen2026-09-25 00:24 UTC136
            transaction_id: event.transaction_id().map(ToOwned::to_owned),
960ac0721079atagen2026-09-25 00:24 UTC137
            sender: sender(event.sender(), event.sender_profile()),
960ac0721079atagen2026-09-25 00:24 UTC138
            timestamp_ms: event.timestamp().get().into(),
960ac0721079atagen2026-09-25 00:24 UTC139
            own: event.is_own(),
960ac0721079atagen2026-09-25 00:24 UTC140
            send_state: send_state(event.send_state()),
960ac0721079atagen2026-09-25 00:24 UTC141
            content: content(event.content(), Some(&media_identity(item, self.scope))),
960ac0721079atagen2026-09-25 00:24 UTC142
            reply_to,
960ac0721079atagen2026-09-25 00:24 UTC143
            thread_root: msg_like.and_then(|content| content.thread_root.clone()),
960ac0721079atagen2026-09-25 00:24 UTC144
            thread_summary: msg_like.and_then(thread_summary),
960ac0721079atagen2026-09-25 00:24 UTC145
            reactions: reactions(event, self.current_user_id),
960ac0721079atagen2026-09-25 00:24 UTC146
            read_by,
960ac0721079atagen2026-09-25 00:24 UTC147
            editable: event.is_editable(),
960ac0721079atagen2026-09-25 00:24 UTC148
        }
960ac0721079atagen2026-09-25 00:24 UTC149
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC150
}
96fdf90aacd6atagen2026-09-08 08:24 UTC151
96fdf90aacd6atagen2026-09-08 08:24 UTC152
pub(crate) fn normalise_timeline<'a>(
96fdf90aacd6atagen2026-09-08 08:24 UTC153
    items: impl Iterator<Item = &'a Arc<SdkTimelineItem>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC154
    current_user_id: Option<&UserId>,
96fdf90aacd6atagen2026-09-08 08:24 UTC155
    scope: &MediaScope,
d9b96952ec60atagen2026-10-25 09:07 UTC156
) -> (NormalisedTimeline, HiddenRows) {
960ac0721079atagen2026-09-25 00:24 UTC157
    let mut normaliser = Normaliser {
960ac0721079atagen2026-09-25 00:24 UTC158
        current_user_id,
960ac0721079atagen2026-09-25 00:24 UTC159
        scope,
96fdf90aacd6atagen2026-09-08 08:24 UTC160
        media: Vec::new(),
960ac0721079atagen2026-09-25 00:24 UTC161
        missing_replies: Vec::new(),
96fdf90aacd6atagen2026-09-08 08:24 UTC162
    };
d9b96952ec60atagen2026-10-25 09:07 UTC163
    let mut hidden = HiddenRows::default();
d9b96952ec60atagen2026-10-25 09:07 UTC164
    let items = hidden.reset(items.map(|item| normaliser.item(item)));
d9b96952ec60atagen2026-10-25 09:07 UTC165
    let normalised = NormalisedTimeline {
d9b96952ec60atagen2026-10-25 09:07 UTC166
        items: items.into_iter().collect(),
960ac0721079atagen2026-09-25 00:24 UTC167
        media: normaliser.media,
960ac0721079atagen2026-09-25 00:24 UTC168
        missing_replies: normaliser.missing_replies,
d9b96952ec60atagen2026-10-25 09:07 UTC169
    };
d9b96952ec60atagen2026-10-25 09:07 UTC170
    (normalised, hidden)
96fdf90aacd6atagen2026-09-08 08:24 UTC171
}
96fdf90aacd6atagen2026-09-08 08:24 UTC172
96fdf90aacd6atagen2026-09-08 08:24 UTC173
pub(crate) fn normalise_timeline_diffs(
96fdf90aacd6atagen2026-09-08 08:24 UTC174
    diffs: Vec<VectorDiff<Arc<SdkTimelineItem>>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC175
    current_user_id: Option<&UserId>,
96fdf90aacd6atagen2026-09-08 08:24 UTC176
    scope: &MediaScope,
d9b96952ec60atagen2026-10-25 09:07 UTC177
    hidden: &mut HiddenRows,
96fdf90aacd6atagen2026-09-08 08:24 UTC178
) -> NormalisedTimelineDiffs {
960ac0721079atagen2026-09-25 00:24 UTC179
    let mut normaliser = Normaliser {
960ac0721079atagen2026-09-25 00:24 UTC180
        current_user_id,
960ac0721079atagen2026-09-25 00:24 UTC181
        scope,
960ac0721079atagen2026-09-25 00:24 UTC182
        media: Vec::new(),
960ac0721079atagen2026-09-25 00:24 UTC183
        missing_replies: Vec::new(),
960ac0721079atagen2026-09-25 00:24 UTC184
    };
96fdf90aacd6atagen2026-09-08 08:24 UTC185
    let diffs = diffs
96fdf90aacd6atagen2026-09-08 08:24 UTC186
        .into_iter()
d9b96952ec60atagen2026-10-25 09:07 UTC187
        .filter_map(|diff| hidden.translate(diff.map(|item| normaliser.item(&item))))
96fdf90aacd6atagen2026-09-08 08:24 UTC188
        .collect();
960ac0721079atagen2026-09-25 00:24 UTC189
    NormalisedTimelineDiffs {
960ac0721079atagen2026-09-25 00:24 UTC190
        diffs,
960ac0721079atagen2026-09-25 00:24 UTC191
        media: normaliser.media,
960ac0721079atagen2026-09-25 00:24 UTC192
        missing_replies: normaliser.missing_replies,
960ac0721079atagen2026-09-25 00:24 UTC193
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC194
}
96fdf90aacd6atagen2026-09-08 08:24 UTC195
d9b96952ec60atagen2026-10-25 09:07 UTC196
/// which sdk rows were left out, so diff indices can step over them
d9b96952ec60atagen2026-10-25 09:07 UTC197
#[derive(Default)]
d9b96952ec60atagen2026-10-25 09:07 UTC198
pub(crate) struct HiddenRows {
d9b96952ec60atagen2026-10-25 09:07 UTC199
    rows: imbl::Vector<bool>,
d9b96952ec60atagen2026-10-25 09:07 UTC200
    hidden: usize,
d9b96952ec60atagen2026-10-25 09:07 UTC201
}
d9b96952ec60atagen2026-10-25 09:07 UTC202
d9b96952ec60atagen2026-10-25 09:07 UTC203
impl HiddenRows {
d9b96952ec60atagen2026-10-25 09:07 UTC204
    fn visible_index(&self, index: usize) -> usize {
d9b96952ec60atagen2026-10-25 09:07 UTC205
        if self.hidden == 0 {
d9b96952ec60atagen2026-10-25 09:07 UTC206
            return index;
d9b96952ec60atagen2026-10-25 09:07 UTC207
        }
d9b96952ec60atagen2026-10-25 09:07 UTC208
        self.rows
d9b96952ec60atagen2026-10-25 09:07 UTC209
            .iter()
d9b96952ec60atagen2026-10-25 09:07 UTC210
            .take(index)
d9b96952ec60atagen2026-10-25 09:07 UTC211
            .filter(|hidden| !**hidden)
d9b96952ec60atagen2026-10-25 09:07 UTC212
            .count()
d9b96952ec60atagen2026-10-25 09:07 UTC213
    }
d9b96952ec60atagen2026-10-25 09:07 UTC214
d9b96952ec60atagen2026-10-25 09:07 UTC215
    fn push(&mut self, front: bool, item: &Option<TimelineItem>) {
d9b96952ec60atagen2026-10-25 09:07 UTC216
        self.hidden += usize::from(item.is_none());
d9b96952ec60atagen2026-10-25 09:07 UTC217
        if front {
d9b96952ec60atagen2026-10-25 09:07 UTC218
            self.rows.push_front(item.is_none());
d9b96952ec60atagen2026-10-25 09:07 UTC219
        } else {
d9b96952ec60atagen2026-10-25 09:07 UTC220
            self.rows.push_back(item.is_none());
d9b96952ec60atagen2026-10-25 09:07 UTC221
        }
d9b96952ec60atagen2026-10-25 09:07 UTC222
    }
d9b96952ec60atagen2026-10-25 09:07 UTC223
d9b96952ec60atagen2026-10-25 09:07 UTC224
    fn forget(&mut self, hidden: bool) -> bool {
d9b96952ec60atagen2026-10-25 09:07 UTC225
        self.hidden -= usize::from(hidden);
d9b96952ec60atagen2026-10-25 09:07 UTC226
        !hidden
d9b96952ec60atagen2026-10-25 09:07 UTC227
    }
d9b96952ec60atagen2026-10-25 09:07 UTC228
d9b96952ec60atagen2026-10-25 09:07 UTC229
    fn reset(&mut self, items: impl Iterator<Item = Option<TimelineItem>>) -> Vec<TimelineItem> {
d9b96952ec60atagen2026-10-25 09:07 UTC230
        *self = Self::default();
d9b96952ec60atagen2026-10-25 09:07 UTC231
        items
d9b96952ec60atagen2026-10-25 09:07 UTC232
            .filter_map(|item| {
d9b96952ec60atagen2026-10-25 09:07 UTC233
                self.push(false, &item);
d9b96952ec60atagen2026-10-25 09:07 UTC234
                item
d9b96952ec60atagen2026-10-25 09:07 UTC235
            })
d9b96952ec60atagen2026-10-25 09:07 UTC236
            .collect()
d9b96952ec60atagen2026-10-25 09:07 UTC237
    }
d9b96952ec60atagen2026-10-25 09:07 UTC238
d9b96952ec60atagen2026-10-25 09:07 UTC239
    fn translate(&mut self, diff: VectorDiff<Option<TimelineItem>>) -> Option<TimelineDiff> {
d9b96952ec60atagen2026-10-25 09:07 UTC240
        Some(match diff {
d9b96952ec60atagen2026-10-25 09:07 UTC241
            VectorDiff::Append { values } => {
d9b96952ec60atagen2026-10-25 09:07 UTC242
                let values: Vec<_> = values
d9b96952ec60atagen2026-10-25 09:07 UTC243
                    .into_iter()
d9b96952ec60atagen2026-10-25 09:07 UTC244
                    .filter_map(|item| {
d9b96952ec60atagen2026-10-25 09:07 UTC245
                        self.push(false, &item);
d9b96952ec60atagen2026-10-25 09:07 UTC246
                        item
d9b96952ec60atagen2026-10-25 09:07 UTC247
                    })
d9b96952ec60atagen2026-10-25 09:07 UTC248
                    .collect();
d9b96952ec60atagen2026-10-25 09:07 UTC249
                if values.is_empty() {
d9b96952ec60atagen2026-10-25 09:07 UTC250
                    return None;
d9b96952ec60atagen2026-10-25 09:07 UTC251
                }
d9b96952ec60atagen2026-10-25 09:07 UTC252
                TimelineDiff::Append { values }
d9b96952ec60atagen2026-10-25 09:07 UTC253
            }
d9b96952ec60atagen2026-10-25 09:07 UTC254
            VectorDiff::Clear => {
d9b96952ec60atagen2026-10-25 09:07 UTC255
                *self = Self::default();
d9b96952ec60atagen2026-10-25 09:07 UTC256
                TimelineDiff::Clear
d9b96952ec60atagen2026-10-25 09:07 UTC257
            }
d9b96952ec60atagen2026-10-25 09:07 UTC258
            VectorDiff::PushFront { value } => {
d9b96952ec60atagen2026-10-25 09:07 UTC259
                self.push(true, &value);
d9b96952ec60atagen2026-10-25 09:07 UTC260
                TimelineDiff::PushFront { value: value? }
d9b96952ec60atagen2026-10-25 09:07 UTC261
            }
d9b96952ec60atagen2026-10-25 09:07 UTC262
            VectorDiff::PushBack { value } => {
d9b96952ec60atagen2026-10-25 09:07 UTC263
                self.push(false, &value);
d9b96952ec60atagen2026-10-25 09:07 UTC264
                TimelineDiff::PushBack { value: value? }
d9b96952ec60atagen2026-10-25 09:07 UTC265
            }
d9b96952ec60atagen2026-10-25 09:07 UTC266
            VectorDiff::PopFront => {
d9b96952ec60atagen2026-10-25 09:07 UTC267
                let hidden = self.rows.pop_front()?;
d9b96952ec60atagen2026-10-25 09:07 UTC268
                self.forget(hidden).then_some(TimelineDiff::PopFront)?
d9b96952ec60atagen2026-10-25 09:07 UTC269
            }
d9b96952ec60atagen2026-10-25 09:07 UTC270
            VectorDiff::PopBack => {
d9b96952ec60atagen2026-10-25 09:07 UTC271
                let hidden = self.rows.pop_back()?;
d9b96952ec60atagen2026-10-25 09:07 UTC272
                self.forget(hidden).then_some(TimelineDiff::PopBack)?
d9b96952ec60atagen2026-10-25 09:07 UTC273
            }
d9b96952ec60atagen2026-10-25 09:07 UTC274
            VectorDiff::Insert { index, value } => {
d9b96952ec60atagen2026-10-25 09:07 UTC275
                let visible = self.visible_index(index);
d9b96952ec60atagen2026-10-25 09:07 UTC276
                self.hidden += usize::from(value.is_none());
d9b96952ec60atagen2026-10-25 09:07 UTC277
                self.rows.insert(index, value.is_none());
d9b96952ec60atagen2026-10-25 09:07 UTC278
                TimelineDiff::Insert {
d9b96952ec60atagen2026-10-25 09:07 UTC279
                    index: visible,
d9b96952ec60atagen2026-10-25 09:07 UTC280
                    value: value?,
d9b96952ec60atagen2026-10-25 09:07 UTC281
                }
d9b96952ec60atagen2026-10-25 09:07 UTC282
            }
d9b96952ec60atagen2026-10-25 09:07 UTC283
            VectorDiff::Set { index, value } => {
d9b96952ec60atagen2026-10-25 09:07 UTC284
                let visible = self.visible_index(index);
d9b96952ec60atagen2026-10-25 09:07 UTC285
                let was_hidden = self.rows.set(index, value.is_none());
d9b96952ec60atagen2026-10-25 09:07 UTC286
                self.hidden = self.hidden + usize::from(value.is_none()) - usize::from(was_hidden);
d9b96952ec60atagen2026-10-25 09:07 UTC287
                match (was_hidden, value) {
d9b96952ec60atagen2026-10-25 09:07 UTC288
                    (false, Some(value)) => TimelineDiff::Set {
d9b96952ec60atagen2026-10-25 09:07 UTC289
                        index: visible,
d9b96952ec60atagen2026-10-25 09:07 UTC290
                        value,
d9b96952ec60atagen2026-10-25 09:07 UTC291
                    },
d9b96952ec60atagen2026-10-25 09:07 UTC292
                    (false, None) => TimelineDiff::Remove { index: visible },
d9b96952ec60atagen2026-10-25 09:07 UTC293
                    (true, Some(value)) => TimelineDiff::Insert {
d9b96952ec60atagen2026-10-25 09:07 UTC294
                        index: visible,
d9b96952ec60atagen2026-10-25 09:07 UTC295
                        value,
d9b96952ec60atagen2026-10-25 09:07 UTC296
                    },
d9b96952ec60atagen2026-10-25 09:07 UTC297
                    (true, None) => return None,
d9b96952ec60atagen2026-10-25 09:07 UTC298
                }
d9b96952ec60atagen2026-10-25 09:07 UTC299
            }
d9b96952ec60atagen2026-10-25 09:07 UTC300
            VectorDiff::Remove { index } => {
d9b96952ec60atagen2026-10-25 09:07 UTC301
                let visible = self.visible_index(index);
d9b96952ec60atagen2026-10-25 09:07 UTC302
                let hidden = self.rows.remove(index);
d9b96952ec60atagen2026-10-25 09:07 UTC303
                self.forget(hidden)
d9b96952ec60atagen2026-10-25 09:07 UTC304
                    .then_some(TimelineDiff::Remove { index: visible })?
d9b96952ec60atagen2026-10-25 09:07 UTC305
            }
d9b96952ec60atagen2026-10-25 09:07 UTC306
            VectorDiff::Truncate { length } => {
d9b96952ec60atagen2026-10-25 09:07 UTC307
                let visible = self.visible_index(length);
d9b96952ec60atagen2026-10-25 09:07 UTC308
                let dropped = self
d9b96952ec60atagen2026-10-25 09:07 UTC309
                    .rows
d9b96952ec60atagen2026-10-25 09:07 UTC310
                    .iter()
d9b96952ec60atagen2026-10-25 09:07 UTC311
                    .skip(length)
d9b96952ec60atagen2026-10-25 09:07 UTC312
                    .filter(|hidden| **hidden)
d9b96952ec60atagen2026-10-25 09:07 UTC313
                    .count();
d9b96952ec60atagen2026-10-25 09:07 UTC314
                self.hidden -= dropped;
d9b96952ec60atagen2026-10-25 09:07 UTC315
                self.rows.truncate(length);
d9b96952ec60atagen2026-10-25 09:07 UTC316
                TimelineDiff::Truncate { length: visible }
d9b96952ec60atagen2026-10-25 09:07 UTC317
            }
d9b96952ec60atagen2026-10-25 09:07 UTC318
            VectorDiff::Reset { values } => TimelineDiff::Reset {
d9b96952ec60atagen2026-10-25 09:07 UTC319
                values: self.reset(values.into_iter()),
d9b96952ec60atagen2026-10-25 09:07 UTC320
            },
d9b96952ec60atagen2026-10-25 09:07 UTC321
        })
d9b96952ec60atagen2026-10-25 09:07 UTC322
    }
d9b96952ec60atagen2026-10-25 09:07 UTC323
}
d9b96952ec60atagen2026-10-25 09:07 UTC324
960ac0721079atagen2026-09-25 00:24 UTC325
/// `media_identity` is `None` for embedded reply content, which never gets fetchable media of its own
960ac0721079atagen2026-09-25 00:24 UTC326
fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> TimelineContent {
960ac0721079atagen2026-09-25 00:24 UTC327
    match content {
960ac0721079atagen2026-09-25 00:24 UTC328
        TimelineItemContent::MsgLike(content) => msg_like(content, media_identity),
960ac0721079atagen2026-09-25 00:24 UTC329
        TimelineItemContent::MembershipChange(change) => TimelineContent::Membership {
960ac0721079atagen2026-09-25 00:24 UTC330
            user_id: change.user_id().to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC331
            display_name: change.display_name(),
960ac0721079atagen2026-09-25 00:24 UTC332
            change: change
960ac0721079atagen2026-09-25 00:24 UTC333
                .change()
960ac0721079atagen2026-09-25 00:24 UTC334
                .map_or(MembershipChange::Other, membership_change),
960ac0721079atagen2026-09-25 00:24 UTC335
        },
960ac0721079atagen2026-09-25 00:24 UTC336
        TimelineItemContent::ProfileChange(change) => TimelineContent::Profile {
960ac0721079atagen2026-09-25 00:24 UTC337
            user_id: change.user_id().to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC338
            display_name: change.displayname_change().map(|change| Change {
960ac0721079atagen2026-09-25 00:24 UTC339
                old: change.old.clone(),
960ac0721079atagen2026-09-25 00:24 UTC340
                new: change.new.clone(),
960ac0721079atagen2026-09-25 00:24 UTC341
            }),
960ac0721079atagen2026-09-25 00:24 UTC342
            avatar: change.avatar_url_change().map(|change| Change {
5d2974de5826atagen2026-10-08 11:29 UTC343
                old: change.old.as_deref().map(MediaId::plain),
5d2974de5826atagen2026-10-08 11:29 UTC344
                new: change.new.as_deref().map(MediaId::plain),
960ac0721079atagen2026-09-25 00:24 UTC345
            }),
960ac0721079atagen2026-09-25 00:24 UTC346
        },
960ac0721079atagen2026-09-25 00:24 UTC347
        TimelineItemContent::OtherState(state) => match state.content() {
5a52012c9b1datagen2026-09-24 23:45 UTC348
            AnyOtherStateEventContentChange::RoomName(change) => TimelineContent::RoomName(
5a52012c9b1datagen2026-09-24 23:45 UTC349
                original(change).and_then(|content| non_empty(&content.name)),
5a52012c9b1datagen2026-09-24 23:45 UTC350
            ),
5a52012c9b1datagen2026-09-24 23:45 UTC351
            AnyOtherStateEventContentChange::RoomTopic(change) => TimelineContent::RoomTopic(
5a52012c9b1datagen2026-09-24 23:45 UTC352
                original(change).and_then(|content| non_empty(&content.topic)),
5a52012c9b1datagen2026-09-24 23:45 UTC353
            ),
960ac0721079atagen2026-09-25 00:24 UTC354
            AnyOtherStateEventContentChange::RoomAvatar(change) => TimelineContent::RoomAvatar(
5d2974de5826atagen2026-10-08 11:29 UTC355
                original(change).and_then(|content| content.url.as_deref().map(MediaId::plain)),
960ac0721079atagen2026-09-25 00:24 UTC356
            ),
960ac0721079atagen2026-09-25 00:24 UTC357
            AnyOtherStateEventContentChange::RoomEncryption(_) => {
960ac0721079atagen2026-09-25 00:24 UTC358
                TimelineContent::EncryptionEnabled
960ac0721079atagen2026-09-25 00:24 UTC359
            }
960ac0721079atagen2026-09-25 00:24 UTC360
            AnyOtherStateEventContentChange::RoomCreate(_) => TimelineContent::RoomCreated,
960ac0721079atagen2026-09-25 00:24 UTC361
            AnyOtherStateEventContentChange::RoomTombstone(change) => match original(change) {
960ac0721079atagen2026-09-25 00:24 UTC362
                Some(content) => TimelineContent::RoomUpgraded {
960ac0721079atagen2026-09-25 00:24 UTC363
                    successor: content.replacement_room.clone(),
960ac0721079atagen2026-09-25 00:24 UTC364
                },
960ac0721079atagen2026-09-25 00:24 UTC365
                None => other_state(state.content().event_type().to_string(), state.state_key()),
960ac0721079atagen2026-09-25 00:24 UTC366
            },
960ac0721079atagen2026-09-25 00:24 UTC367
            other => other_state(other.event_type().to_string(), state.state_key()),
960ac0721079atagen2026-09-25 00:24 UTC368
        },
960ac0721079atagen2026-09-25 00:24 UTC369
        TimelineItemContent::FailedToParseMessageLike { event_type, .. } => {
960ac0721079atagen2026-09-25 00:24 UTC370
            TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC371
                event_type: event_type.to_string(),
960ac0721079atagen2026-09-25 00:24 UTC372
            }
960ac0721079atagen2026-09-25 00:24 UTC373
        }
960ac0721079atagen2026-09-25 00:24 UTC374
        TimelineItemContent::FailedToParseState {
960ac0721079atagen2026-09-25 00:24 UTC375
            event_type,
960ac0721079atagen2026-09-25 00:24 UTC376
            state_key,
960ac0721079atagen2026-09-25 00:24 UTC377
            ..
960ac0721079atagen2026-09-25 00:24 UTC378
        } => other_state(event_type.to_string(), state_key),
960ac0721079atagen2026-09-25 00:24 UTC379
        TimelineItemContent::CallInvite => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC380
            event_type: "m.call.invite".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC381
        },
960ac0721079atagen2026-09-25 00:24 UTC382
        TimelineItemContent::RtcNotification { .. } => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC383
            event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC384
        },
960ac0721079atagen2026-09-25 00:24 UTC385
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC386
}
96fdf90aacd6atagen2026-09-08 08:24 UTC387
5a52012c9b1datagen2026-09-24 23:45 UTC388
/// a cleared name or topic arrives as an empty string
5a52012c9b1datagen2026-09-24 23:45 UTC389
fn non_empty(value: &str) -> Option<String> {
5a52012c9b1datagen2026-09-24 23:45 UTC390
    (!value.is_empty()).then(|| value.to_owned())
5a52012c9b1datagen2026-09-24 23:45 UTC391
}
5a52012c9b1datagen2026-09-24 23:45 UTC392
960ac0721079atagen2026-09-25 00:24 UTC393
fn other_state(event_type: String, state_key: &str) -> TimelineContent {
960ac0721079atagen2026-09-25 00:24 UTC394
    TimelineContent::OtherState {
960ac0721079atagen2026-09-25 00:24 UTC395
        event_type,
960ac0721079atagen2026-09-25 00:24 UTC396
        state_key: state_key.to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC397
    }
960ac0721079atagen2026-09-25 00:24 UTC398
}
960ac0721079atagen2026-09-25 00:24 UTC399
960ac0721079atagen2026-09-25 00:24 UTC400
fn original<C>(change: &StateEventContentChange<C>) -> Option<&C>
960ac0721079atagen2026-09-25 00:24 UTC401
where
960ac0721079atagen2026-09-25 00:24 UTC402
    C: ruma::events::StaticStateEventContent + ruma::events::RedactContent,
960ac0721079atagen2026-09-25 00:24 UTC403
{
960ac0721079atagen2026-09-25 00:24 UTC404
    match change {
960ac0721079atagen2026-09-25 00:24 UTC405
        StateEventContentChange::Original { content, .. } => Some(content),
960ac0721079atagen2026-09-25 00:24 UTC406
        StateEventContentChange::Redacted(_) => None,
960ac0721079atagen2026-09-25 00:24 UTC407
    }
960ac0721079atagen2026-09-25 00:24 UTC408
}
960ac0721079atagen2026-09-25 00:24 UTC409
960ac0721079atagen2026-09-25 00:24 UTC410
fn msg_like(content: &MsgLikeContent, media_identity: Option<&str>) -> TimelineContent {
960ac0721079atagen2026-09-25 00:24 UTC411
    match &content.kind {
960ac0721079atagen2026-09-25 00:24 UTC412
        MsgLikeKind::Message(message) => TimelineContent::Message(Message {
960ac0721079atagen2026-09-25 00:24 UTC413
            kind: message_kind(message.msgtype()),
960ac0721079atagen2026-09-25 00:24 UTC414
            body: message.body().to_owned(),
5a52012c9b1datagen2026-09-24 23:45 UTC415
            geo_uri: match message.msgtype() {
5a52012c9b1datagen2026-09-24 23:45 UTC416
                MessageType::Location(location) => Some(location.geo_uri.clone()),
5a52012c9b1datagen2026-09-24 23:45 UTC417
                _ => None,
5a52012c9b1datagen2026-09-24 23:45 UTC418
            },
960ac0721079atagen2026-09-25 00:24 UTC419
            formatted_body: formatted_body(message.msgtype()),
960ac0721079atagen2026-09-25 00:24 UTC420
            mentions: message.mentions().map(|mentions| MessageMentions {
960ac0721079atagen2026-09-25 00:24 UTC421
                user_ids: mentions.user_ids.iter().cloned().collect(),
960ac0721079atagen2026-09-25 00:24 UTC422
                room: mentions.room,
960ac0721079atagen2026-09-25 00:24 UTC423
            }),
960ac0721079atagen2026-09-25 00:24 UTC424
            edited: message.is_edited(),
960ac0721079atagen2026-09-25 00:24 UTC425
            media: media_identity
960ac0721079atagen2026-09-25 00:24 UTC426
                .and_then(|identity| media_from_message(identity, message.msgtype())),
0291516d40b5atagen2026-10-22 08:31 UTC427
            link_previews: sender_previews(media_identity, message.msgtype())
0291516d40b5atagen2026-10-22 08:31 UTC428
                .into_iter()
0291516d40b5atagen2026-10-22 08:31 UTC429
                .map(|(preview, _)| preview)
0291516d40b5atagen2026-10-22 08:31 UTC430
                .collect(),
960ac0721079atagen2026-09-25 00:24 UTC431
        }),
960ac0721079atagen2026-09-25 00:24 UTC432
        MsgLikeKind::Sticker(sticker) => {
960ac0721079atagen2026-09-25 00:24 UTC433
            let sticker = sticker.content();
960ac0721079atagen2026-09-25 00:24 UTC434
            match media_identity {
960ac0721079atagen2026-09-25 00:24 UTC435
                Some(identity) => TimelineContent::Sticker {
960ac0721079atagen2026-09-25 00:24 UTC436
                    body: sticker.body.clone(),
960ac0721079atagen2026-09-25 00:24 UTC437
                    media: media_descriptor_for_sticker(identity, sticker),
960ac0721079atagen2026-09-25 00:24 UTC438
                },
960ac0721079atagen2026-09-25 00:24 UTC439
                None => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC440
                    event_type: "m.sticker".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC441
                },
96fdf90aacd6atagen2026-09-08 08:24 UTC442
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC443
        }
960ac0721079atagen2026-09-25 00:24 UTC444
        MsgLikeKind::Poll(poll) => TimelineContent::Poll {
960ac0721079atagen2026-09-25 00:24 UTC445
            question: poll.results().question,
960ac0721079atagen2026-09-25 00:24 UTC446
        },
960ac0721079atagen2026-09-25 00:24 UTC447
        MsgLikeKind::Redacted => TimelineContent::Redacted,
960ac0721079atagen2026-09-25 00:24 UTC448
        MsgLikeKind::UnableToDecrypt(encrypted) => {
960ac0721079atagen2026-09-25 00:24 UTC449
            TimelineContent::UnableToDecrypt(match encrypted {
960ac0721079atagen2026-09-25 00:24 UTC450
                EncryptedMessage::MegolmV1AesSha2 { cause, .. } => utd_cause(*cause),
960ac0721079atagen2026-09-25 00:24 UTC451
                _ => UtdCause::Unknown,
960ac0721079atagen2026-09-25 00:24 UTC452
            })
96fdf90aacd6atagen2026-09-08 08:24 UTC453
        }
960ac0721079atagen2026-09-25 00:24 UTC454
        MsgLikeKind::Other(other) => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC455
            event_type: other.event_type().to_string(),
960ac0721079atagen2026-09-25 00:24 UTC456
        },
960ac0721079atagen2026-09-25 00:24 UTC457
        MsgLikeKind::LiveLocation(_) => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC458
            event_type: "m.beacon_info".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC459
        },
96fdf90aacd6atagen2026-09-08 08:24 UTC460
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC461
}
96fdf90aacd6atagen2026-09-08 08:24 UTC462
5a52012c9b1datagen2026-09-24 23:45 UTC463
pub(crate) fn message_kind(message: &MessageType) -> MessageKind {
960ac0721079atagen2026-09-25 00:24 UTC464
    match message {
960ac0721079atagen2026-09-25 00:24 UTC465
        MessageType::Text(_) => MessageKind::Text,
960ac0721079atagen2026-09-25 00:24 UTC466
        MessageType::Emote(_) => MessageKind::Emote,
960ac0721079atagen2026-09-25 00:24 UTC467
        MessageType::Notice(_) => MessageKind::Notice,
960ac0721079atagen2026-09-25 00:24 UTC468
        MessageType::ServerNotice(_) => MessageKind::ServerNotice,
960ac0721079atagen2026-09-25 00:24 UTC469
        MessageType::Image(_) => MessageKind::Image,
960ac0721079atagen2026-09-25 00:24 UTC470
        MessageType::Video(_) => MessageKind::Video,
960ac0721079atagen2026-09-25 00:24 UTC471
        MessageType::Audio(_) => MessageKind::Audio,
960ac0721079atagen2026-09-25 00:24 UTC472
        MessageType::File(_) => MessageKind::File,
960ac0721079atagen2026-09-25 00:24 UTC473
        MessageType::Location(_) => MessageKind::Location,
960ac0721079atagen2026-09-25 00:24 UTC474
        MessageType::VerificationRequest(_) => MessageKind::VerificationRequest,
960ac0721079atagen2026-09-25 00:24 UTC475
        other => MessageKind::Other(other.msgtype().to_owned()),
960ac0721079atagen2026-09-25 00:24 UTC476
    }
960ac0721079atagen2026-09-25 00:24 UTC477
}
960ac0721079atagen2026-09-25 00:24 UTC478
960ac0721079atagen2026-09-25 00:24 UTC479
const fn membership_change(change: SdkMembershipChange) -> MembershipChange {
960ac0721079atagen2026-09-25 00:24 UTC480
    match change {
960ac0721079atagen2026-09-25 00:24 UTC481
        SdkMembershipChange::Joined => MembershipChange::Joined,
960ac0721079atagen2026-09-25 00:24 UTC482
        SdkMembershipChange::Left => MembershipChange::Left,
960ac0721079atagen2026-09-25 00:24 UTC483
        SdkMembershipChange::Banned => MembershipChange::Banned,
960ac0721079atagen2026-09-25 00:24 UTC484
        SdkMembershipChange::Unbanned => MembershipChange::Unbanned,
960ac0721079atagen2026-09-25 00:24 UTC485
        SdkMembershipChange::Kicked => MembershipChange::Kicked,
960ac0721079atagen2026-09-25 00:24 UTC486
        SdkMembershipChange::KickedAndBanned => MembershipChange::KickedAndBanned,
960ac0721079atagen2026-09-25 00:24 UTC487
        SdkMembershipChange::Invited => MembershipChange::Invited,
960ac0721079atagen2026-09-25 00:24 UTC488
        SdkMembershipChange::InvitationAccepted => MembershipChange::InvitationAccepted,
960ac0721079atagen2026-09-25 00:24 UTC489
        SdkMembershipChange::InvitationRejected => MembershipChange::InvitationRejected,
960ac0721079atagen2026-09-25 00:24 UTC490
        SdkMembershipChange::InvitationRevoked => MembershipChange::InvitationRevoked,
960ac0721079atagen2026-09-25 00:24 UTC491
        SdkMembershipChange::Knocked => MembershipChange::Knocked,
960ac0721079atagen2026-09-25 00:24 UTC492
        SdkMembershipChange::KnockAccepted => MembershipChange::KnockAccepted,
960ac0721079atagen2026-09-25 00:24 UTC493
        SdkMembershipChange::KnockRetracted => MembershipChange::KnockRetracted,
960ac0721079atagen2026-09-25 00:24 UTC494
        SdkMembershipChange::KnockDenied => MembershipChange::KnockDenied,
960ac0721079atagen2026-09-25 00:24 UTC495
        SdkMembershipChange::None
960ac0721079atagen2026-09-25 00:24 UTC496
        | SdkMembershipChange::Error
960ac0721079atagen2026-09-25 00:24 UTC497
        | SdkMembershipChange::NotImplemented => MembershipChange::Other,
960ac0721079atagen2026-09-25 00:24 UTC498
    }
960ac0721079atagen2026-09-25 00:24 UTC499
}
960ac0721079atagen2026-09-25 00:24 UTC500
960ac0721079atagen2026-09-25 00:24 UTC501
const fn utd_cause(cause: SdkUtdCause) -> UtdCause {
960ac0721079atagen2026-09-25 00:24 UTC502
    match cause {
960ac0721079atagen2026-09-25 00:24 UTC503
        SdkUtdCause::Unknown => UtdCause::Unknown,
960ac0721079atagen2026-09-25 00:24 UTC504
        SdkUtdCause::SentBeforeWeJoined => UtdCause::SentBeforeWeJoined,
960ac0721079atagen2026-09-25 00:24 UTC505
        SdkUtdCause::VerificationViolation => UtdCause::VerificationViolation,
960ac0721079atagen2026-09-25 00:24 UTC506
        SdkUtdCause::UnsignedDevice => UtdCause::UnsignedDevice,
960ac0721079atagen2026-09-25 00:24 UTC507
        SdkUtdCause::UnknownDevice => UtdCause::UnknownDevice,
960ac0721079atagen2026-09-25 00:24 UTC508
        SdkUtdCause::HistoricalMessageAndBackupIsDisabled => {
960ac0721079atagen2026-09-25 00:24 UTC509
            UtdCause::HistoricalMessageAndBackupIsDisabled
960ac0721079atagen2026-09-25 00:24 UTC510
        }
960ac0721079atagen2026-09-25 00:24 UTC511
        SdkUtdCause::WithheldForUnverifiedOrInsecureDevice => {
960ac0721079atagen2026-09-25 00:24 UTC512
            UtdCause::WithheldForUnverifiedOrInsecureDevice
960ac0721079atagen2026-09-25 00:24 UTC513
        }
960ac0721079atagen2026-09-25 00:24 UTC514
        SdkUtdCause::WithheldBySender => UtdCause::WithheldBySender,
960ac0721079atagen2026-09-25 00:24 UTC515
        SdkUtdCause::HistoricalMessageAndDeviceIsUnverified => {
960ac0721079atagen2026-09-25 00:24 UTC516
            UtdCause::HistoricalMessageAndDeviceIsUnverified
960ac0721079atagen2026-09-25 00:24 UTC517
        }
960ac0721079atagen2026-09-25 00:24 UTC518
    }
960ac0721079atagen2026-09-25 00:24 UTC519
}
960ac0721079atagen2026-09-25 00:24 UTC520
960ac0721079atagen2026-09-25 00:24 UTC521
fn sender(user_id: &UserId, profile: &TimelineDetails<Profile>) -> Sender {
960ac0721079atagen2026-09-25 00:24 UTC522
    let profile = match profile {
960ac0721079atagen2026-09-25 00:24 UTC523
        TimelineDetails::Ready(profile) => Some(profile),
96fdf90aacd6atagen2026-09-08 08:24 UTC524
        _ => None,
96fdf90aacd6atagen2026-09-08 08:24 UTC525
    };
960ac0721079atagen2026-09-25 00:24 UTC526
    Sender {
960ac0721079atagen2026-09-25 00:24 UTC527
        user_id: user_id.to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC528
        display_name: profile.and_then(|profile| profile.display_name.clone()),
5d2974de5826atagen2026-10-08 11:29 UTC529
        avatar: profile.and_then(|profile| profile.avatar_url.as_deref().map(MediaId::plain)),
960ac0721079atagen2026-09-25 00:24 UTC530
        name_ambiguous: profile.is_some_and(|profile| profile.display_name_ambiguous),
960ac0721079atagen2026-09-25 00:24 UTC531
    }
960ac0721079atagen2026-09-25 00:24 UTC532
}
960ac0721079atagen2026-09-25 00:24 UTC533
960ac0721079atagen2026-09-25 00:24 UTC534
fn send_state(state: Option<&EventSendState>) -> SendState {
960ac0721079atagen2026-09-25 00:24 UTC535
    match state {
960ac0721079atagen2026-09-25 00:24 UTC536
        None | Some(EventSendState::Sent { .. }) => SendState::Sent,
5a52012c9b1datagen2026-09-24 23:45 UTC537
        Some(EventSendState::NotSentYet { .. }) => SendState::Sending,
5a52012c9b1datagen2026-09-24 23:45 UTC538
        // recoverable means the sdk parks the queue and we unpark it, nothing for the user to do
960ac0721079atagen2026-09-25 00:24 UTC539
        Some(EventSendState::SendingFailed { is_recoverable, .. }) => SendState::Failed {
5a52012c9b1datagen2026-09-24 23:45 UTC540
            will_retry: *is_recoverable,
960ac0721079atagen2026-09-25 00:24 UTC541
        },
960ac0721079atagen2026-09-25 00:24 UTC542
    }
960ac0721079atagen2026-09-25 00:24 UTC543
}
960ac0721079atagen2026-09-25 00:24 UTC544
960ac0721079atagen2026-09-25 00:24 UTC545
fn thread_summary(content: &MsgLikeContent) -> Option<ThreadSummary> {
960ac0721079atagen2026-09-25 00:24 UTC546
    content.thread_summary.as_ref().map(|summary| {
960ac0721079atagen2026-09-25 00:24 UTC547
        let latest_event = match &summary.latest_event {
960ac0721079atagen2026-09-25 00:24 UTC548
            TimelineDetails::Ready(event) => Some(event.as_ref()),
960ac0721079atagen2026-09-25 00:24 UTC549
            _ => None,
960ac0721079atagen2026-09-25 00:24 UTC550
        };
960ac0721079atagen2026-09-25 00:24 UTC551
        ThreadSummary {
960ac0721079atagen2026-09-25 00:24 UTC552
            reply_count: summary.num_replies,
960ac0721079atagen2026-09-25 00:24 UTC553
            latest_event_id: latest_event.and_then(|event| match &event.identifier {
960ac0721079atagen2026-09-25 00:24 UTC554
                TimelineEventItemId::EventId(event_id) => Some(event_id.clone()),
960ac0721079atagen2026-09-25 00:24 UTC555
                TimelineEventItemId::TransactionId(_) => None,
96fdf90aacd6atagen2026-09-08 08:24 UTC556
            }),
960ac0721079atagen2026-09-25 00:24 UTC557
            latest_event_timestamp_ms: latest_event.map(|event| event.timestamp.get().into()),
960ac0721079atagen2026-09-25 00:24 UTC558
        }
960ac0721079atagen2026-09-25 00:24 UTC559
    })
960ac0721079atagen2026-09-25 00:24 UTC560
}
960ac0721079atagen2026-09-25 00:24 UTC561
960ac0721079atagen2026-09-25 00:24 UTC562
fn reactions(event: &EventTimelineItem, current_user_id: Option<&UserId>) -> Vec<Reaction> {
960ac0721079atagen2026-09-25 00:24 UTC563
    event
96fdf90aacd6atagen2026-09-08 08:24 UTC564
        .content()
96fdf90aacd6atagen2026-09-08 08:24 UTC565
        .reactions()
96fdf90aacd6atagen2026-09-08 08:24 UTC566
        .into_iter()
96fdf90aacd6atagen2026-09-08 08:24 UTC567
        .flat_map(|reactions| reactions.iter())
96fdf90aacd6atagen2026-09-08 08:24 UTC568
        .map(|(key, senders)| Reaction {
96fdf90aacd6atagen2026-09-08 08:24 UTC569
            key: key.clone(),
8fa8a7cde89catagen2026-10-08 12:39 UTC570
            shortcode: senders.values().find_map(|info| info.shortcode.clone()),
8fa8a7cde89catagen2026-10-08 12:39 UTC571
            image: <&MxcUri>::from(key.as_str())
8fa8a7cde89catagen2026-10-08 12:39 UTC572
                .is_valid()
8fa8a7cde89catagen2026-10-08 12:39 UTC573
                .then(|| MediaId::plain(key)),
96fdf90aacd6atagen2026-09-08 08:24 UTC574
            count: senders.len(),
96fdf90aacd6atagen2026-09-08 08:24 UTC575
            own: current_user_id.is_some_and(|user_id| senders.contains_key(user_id)),
96fdf90aacd6atagen2026-09-08 08:24 UTC576
            senders: senders.keys().cloned().collect(),
96fdf90aacd6atagen2026-09-08 08:24 UTC577
        })
960ac0721079atagen2026-09-25 00:24 UTC578
        .collect()
96fdf90aacd6atagen2026-09-08 08:24 UTC579
}
96fdf90aacd6atagen2026-09-08 08:24 UTC580
cbb715e11963atagen2026-10-24 23:24 UTC581
pub(super) fn formatted_body(message: &MessageType) -> Option<String> {
96fdf90aacd6atagen2026-09-08 08:24 UTC582
    let formatted = match message {
96fdf90aacd6atagen2026-09-08 08:24 UTC583
        MessageType::Text(content) => content.formatted.as_ref(),
96fdf90aacd6atagen2026-09-08 08:24 UTC584
        MessageType::Emote(content) => content.formatted.as_ref(),
96fdf90aacd6atagen2026-09-08 08:24 UTC585
        MessageType::Notice(content) => content.formatted.as_ref(),
96fdf90aacd6atagen2026-09-08 08:24 UTC586
        MessageType::Audio(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC587
        MessageType::File(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC588
        MessageType::Image(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC589
        MessageType::Video(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC590
        _ => None,
96fdf90aacd6atagen2026-09-08 08:24 UTC591
    }?;
96fdf90aacd6atagen2026-09-08 08:24 UTC592
    (formatted.format == MessageFormat::Html).then(|| formatted.body.clone())
96fdf90aacd6atagen2026-09-08 08:24 UTC593
}
d9b96952ec60atagen2026-10-25 09:07 UTC594
d9b96952ec60atagen2026-10-25 09:07 UTC595
#[cfg(test)]
d9b96952ec60atagen2026-10-25 09:07 UTC596
mod tests {
d9b96952ec60atagen2026-10-25 09:07 UTC597
    use super::*;
d9b96952ec60atagen2026-10-25 09:07 UTC598
d9b96952ec60atagen2026-10-25 09:07 UTC599
    fn row(n: u64) -> Option<TimelineItem> {
d9b96952ec60atagen2026-10-25 09:07 UTC600
        Some(TimelineItem::DayDivider { timestamp_ms: n })
d9b96952ec60atagen2026-10-25 09:07 UTC601
    }
d9b96952ec60atagen2026-10-25 09:07 UTC602
d9b96952ec60atagen2026-10-25 09:07 UTC603
    #[test]
d9b96952ec60atagen2026-10-25 09:07 UTC604
    fn hidden_rows_keep_diff_indices_on_the_visible_timeline() {
d9b96952ec60atagen2026-10-25 09:07 UTC605
        let mut sdk: imbl::Vector<Option<TimelineItem>> = imbl::Vector::new();
d9b96952ec60atagen2026-10-25 09:07 UTC606
        let mut hidden = HiddenRows::default();
d9b96952ec60atagen2026-10-25 09:07 UTC607
        let mut visible: imbl::Vector<TimelineItem> = imbl::Vector::new();
d9b96952ec60atagen2026-10-25 09:07 UTC608
        let diffs = [
d9b96952ec60atagen2026-10-25 09:07 UTC609
            VectorDiff::Reset {
d9b96952ec60atagen2026-10-25 09:07 UTC610
                values: [row(0), None, row(1)].into_iter().collect(),
d9b96952ec60atagen2026-10-25 09:07 UTC611
            },
d9b96952ec60atagen2026-10-25 09:07 UTC612
            VectorDiff::Insert {
d9b96952ec60atagen2026-10-25 09:07 UTC613
                index: 1,
d9b96952ec60atagen2026-10-25 09:07 UTC614
                value: row(2),
d9b96952ec60atagen2026-10-25 09:07 UTC615
            },
d9b96952ec60atagen2026-10-25 09:07 UTC616
            VectorDiff::Set {
d9b96952ec60atagen2026-10-25 09:07 UTC617
                index: 2,
d9b96952ec60atagen2026-10-25 09:07 UTC618
                value: row(3),
d9b96952ec60atagen2026-10-25 09:07 UTC619
            },
d9b96952ec60atagen2026-10-25 09:07 UTC620
            VectorDiff::Set {
d9b96952ec60atagen2026-10-25 09:07 UTC621
                index: 0,
d9b96952ec60atagen2026-10-25 09:07 UTC622
                value: None,
d9b96952ec60atagen2026-10-25 09:07 UTC623
            },
d9b96952ec60atagen2026-10-25 09:07 UTC624
            VectorDiff::PushFront { value: None },
d9b96952ec60atagen2026-10-25 09:07 UTC625
            VectorDiff::Remove { index: 3 },
d9b96952ec60atagen2026-10-25 09:07 UTC626
            VectorDiff::Append {
d9b96952ec60atagen2026-10-25 09:07 UTC627
                values: [None, row(4)].into_iter().collect(),
d9b96952ec60atagen2026-10-25 09:07 UTC628
            },
d9b96952ec60atagen2026-10-25 09:07 UTC629
            VectorDiff::PopFront,
d9b96952ec60atagen2026-10-25 09:07 UTC630
            VectorDiff::Truncate { length: 2 },
d9b96952ec60atagen2026-10-25 09:07 UTC631
            VectorDiff::PopBack,
d9b96952ec60atagen2026-10-25 09:07 UTC632
        ];
d9b96952ec60atagen2026-10-25 09:07 UTC633
        for diff in diffs {
d9b96952ec60atagen2026-10-25 09:07 UTC634
            diff.clone().apply(&mut sdk);
d9b96952ec60atagen2026-10-25 09:07 UTC635
            if let Some(diff) = hidden.translate(diff) {
d9b96952ec60atagen2026-10-25 09:07 UTC636
                diff.try_apply(&mut visible).unwrap();
d9b96952ec60atagen2026-10-25 09:07 UTC637
            }
d9b96952ec60atagen2026-10-25 09:07 UTC638
            let expected: imbl::Vector<_> = sdk.iter().flatten().cloned().collect();
d9b96952ec60atagen2026-10-25 09:07 UTC639
            assert_eq!(visible, expected);
d9b96952ec60atagen2026-10-25 09:07 UTC640
        }
d9b96952ec60atagen2026-10-25 09:07 UTC641
    }
d9b96952ec60atagen2026-10-25 09:07 UTC642
}