atagen

macltok

public
Star0
Watch0
9c05b1dd327a57081652545c709cc8d7e961de15
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::{
960ac0721079atagen2026-09-25 00:24 UTC14
    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::{
960ac0721079atagen2026-09-25 00:24 UTC22
    Change, MembershipChange, Message, MessageKind, MessageMentions, Reaction, ReplyDetails,
960ac0721079atagen2026-09-25 00:24 UTC23
    ReplyReference, SendState, Sender, ThreadSummary, TimelineContent, TimelineDiff, TimelineEvent,
960ac0721079atagen2026-09-25 00:24 UTC24
    TimelineItem, TimelineItemId, UtdCause,
96fdf90aacd6atagen2026-09-08 08:24 UTC25
};
96fdf90aacd6atagen2026-09-08 08:24 UTC26
96fdf90aacd6atagen2026-09-08 08:24 UTC27
use super::media::{
96fdf90aacd6atagen2026-09-08 08:24 UTC28
    MediaRegistration, MediaScope, media_descriptor_for_sticker, media_from_message,
96fdf90aacd6atagen2026-09-08 08:24 UTC29
    media_identity, media_registration_for_item,
96fdf90aacd6atagen2026-09-08 08:24 UTC30
};
96fdf90aacd6atagen2026-09-08 08:24 UTC31
960ac0721079atagen2026-09-25 00:24 UTC32
#[derive(Default)]
96fdf90aacd6atagen2026-09-08 08:24 UTC33
pub(crate) struct NormalisedTimeline {
d5d8097c3afdatagen2026-09-26 09:33 UTC34
    pub(crate) items: imbl::Vector<TimelineItem>,
96fdf90aacd6atagen2026-09-08 08:24 UTC35
    pub(crate) media: Vec<MediaRegistration>,
960ac0721079atagen2026-09-25 00:24 UTC36
    /// replies whose target isn't loaded, so somebody should go and ask for it
960ac0721079atagen2026-09-25 00:24 UTC37
    pub(crate) missing_replies: Vec<OwnedEventId>,
960ac0721079atagen2026-09-25 00:24 UTC38
}
960ac0721079atagen2026-09-25 00:24 UTC39
960ac0721079atagen2026-09-25 00:24 UTC40
pub(crate) struct NormalisedTimelineDiffs {
960ac0721079atagen2026-09-25 00:24 UTC41
    pub(crate) diffs: Vec<TimelineDiff>,
960ac0721079atagen2026-09-25 00:24 UTC42
    pub(crate) media: Vec<MediaRegistration>,
960ac0721079atagen2026-09-25 00:24 UTC43
    pub(crate) missing_replies: Vec<OwnedEventId>,
960ac0721079atagen2026-09-25 00:24 UTC44
}
960ac0721079atagen2026-09-25 00:24 UTC45
960ac0721079atagen2026-09-25 00:24 UTC46
struct Normaliser<'a> {
960ac0721079atagen2026-09-25 00:24 UTC47
    current_user_id: Option<&'a UserId>,
960ac0721079atagen2026-09-25 00:24 UTC48
    scope: &'a MediaScope,
960ac0721079atagen2026-09-25 00:24 UTC49
    media: Vec<MediaRegistration>,
960ac0721079atagen2026-09-25 00:24 UTC50
    missing_replies: Vec<OwnedEventId>,
960ac0721079atagen2026-09-25 00:24 UTC51
}
960ac0721079atagen2026-09-25 00:24 UTC52
960ac0721079atagen2026-09-25 00:24 UTC53
impl Normaliser<'_> {
960ac0721079atagen2026-09-25 00:24 UTC54
    fn item(&mut self, item: &SdkTimelineItem) -> TimelineItem {
960ac0721079atagen2026-09-25 00:24 UTC55
        if let Some(media) = media_registration_for_item(item, self.scope) {
960ac0721079atagen2026-09-25 00:24 UTC56
            self.media.push(media);
960ac0721079atagen2026-09-25 00:24 UTC57
        }
960ac0721079atagen2026-09-25 00:24 UTC58
        match item.kind() {
960ac0721079atagen2026-09-25 00:24 UTC59
            TimelineItemKind::Event(event) => {
960ac0721079atagen2026-09-25 00:24 UTC60
                TimelineItem::Event(Box::new(self.event(item, event)))
960ac0721079atagen2026-09-25 00:24 UTC61
            }
960ac0721079atagen2026-09-25 00:24 UTC62
            TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => {
960ac0721079atagen2026-09-25 00:24 UTC63
                TimelineItem::DayDivider {
960ac0721079atagen2026-09-25 00:24 UTC64
                    timestamp_ms: timestamp.get().into(),
960ac0721079atagen2026-09-25 00:24 UTC65
                }
960ac0721079atagen2026-09-25 00:24 UTC66
            }
960ac0721079atagen2026-09-25 00:24 UTC67
            TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => TimelineItem::ReadMarker,
960ac0721079atagen2026-09-25 00:24 UTC68
            TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {
960ac0721079atagen2026-09-25 00:24 UTC69
                TimelineItem::TimelineStart
960ac0721079atagen2026-09-25 00:24 UTC70
            }
960ac0721079atagen2026-09-25 00:24 UTC71
        }
960ac0721079atagen2026-09-25 00:24 UTC72
    }
960ac0721079atagen2026-09-25 00:24 UTC73
960ac0721079atagen2026-09-25 00:24 UTC74
    fn event(&mut self, item: &SdkTimelineItem, event: &EventTimelineItem) -> TimelineEvent {
960ac0721079atagen2026-09-25 00:24 UTC75
        let msg_like = match event.content() {
960ac0721079atagen2026-09-25 00:24 UTC76
            TimelineItemContent::MsgLike(content) => Some(content),
960ac0721079atagen2026-09-25 00:24 UTC77
            _ => None,
960ac0721079atagen2026-09-25 00:24 UTC78
        };
960ac0721079atagen2026-09-25 00:24 UTC79
        let reply_to = msg_like
960ac0721079atagen2026-09-25 00:24 UTC80
            .and_then(|content| content.in_reply_to.as_ref())
960ac0721079atagen2026-09-25 00:24 UTC81
            .map(|reply| {
960ac0721079atagen2026-09-25 00:24 UTC82
                let details = match &reply.event {
960ac0721079atagen2026-09-25 00:24 UTC83
                    TimelineDetails::Ready(embedded) => ReplyDetails::Ready {
960ac0721079atagen2026-09-25 00:24 UTC84
                        sender: sender(&embedded.sender, &embedded.sender_profile),
960ac0721079atagen2026-09-25 00:24 UTC85
                        content: Box::new(content(&embedded.content, None)),
960ac0721079atagen2026-09-25 00:24 UTC86
                        timestamp_ms: embedded.timestamp.get().into(),
960ac0721079atagen2026-09-25 00:24 UTC87
                    },
960ac0721079atagen2026-09-25 00:24 UTC88
                    TimelineDetails::Unavailable => {
960ac0721079atagen2026-09-25 00:24 UTC89
                        self.missing_replies.push(reply.event_id.clone());
960ac0721079atagen2026-09-25 00:24 UTC90
                        ReplyDetails::Pending
960ac0721079atagen2026-09-25 00:24 UTC91
                    }
960ac0721079atagen2026-09-25 00:24 UTC92
                    TimelineDetails::Pending => ReplyDetails::Pending,
960ac0721079atagen2026-09-25 00:24 UTC93
                    TimelineDetails::Error(_) => ReplyDetails::Unavailable,
960ac0721079atagen2026-09-25 00:24 UTC94
                };
960ac0721079atagen2026-09-25 00:24 UTC95
                ReplyReference {
960ac0721079atagen2026-09-25 00:24 UTC96
                    event_id: reply.event_id.clone(),
960ac0721079atagen2026-09-25 00:24 UTC97
                    details,
960ac0721079atagen2026-09-25 00:24 UTC98
                }
960ac0721079atagen2026-09-25 00:24 UTC99
            });
960ac0721079atagen2026-09-25 00:24 UTC100
        let mut read_by: Vec<_> = event
960ac0721079atagen2026-09-25 00:24 UTC101
            .read_receipts()
960ac0721079atagen2026-09-25 00:24 UTC102
            .keys()
960ac0721079atagen2026-09-25 00:24 UTC103
            .filter(|user_id| *user_id != event.sender())
960ac0721079atagen2026-09-25 00:24 UTC104
            .cloned()
960ac0721079atagen2026-09-25 00:24 UTC105
            .collect();
960ac0721079atagen2026-09-25 00:24 UTC106
        read_by.sort();
960ac0721079atagen2026-09-25 00:24 UTC107
        TimelineEvent {
960ac0721079atagen2026-09-25 00:24 UTC108
            item_id: TimelineItemId::new(item.unique_id().0.clone()),
960ac0721079atagen2026-09-25 00:24 UTC109
            event_id: event.event_id().map(ToOwned::to_owned),
960ac0721079atagen2026-09-25 00:24 UTC110
            transaction_id: event.transaction_id().map(ToOwned::to_owned),
960ac0721079atagen2026-09-25 00:24 UTC111
            sender: sender(event.sender(), event.sender_profile()),
960ac0721079atagen2026-09-25 00:24 UTC112
            timestamp_ms: event.timestamp().get().into(),
960ac0721079atagen2026-09-25 00:24 UTC113
            own: event.is_own(),
960ac0721079atagen2026-09-25 00:24 UTC114
            send_state: send_state(event.send_state()),
960ac0721079atagen2026-09-25 00:24 UTC115
            content: content(event.content(), Some(&media_identity(item, self.scope))),
960ac0721079atagen2026-09-25 00:24 UTC116
            reply_to,
960ac0721079atagen2026-09-25 00:24 UTC117
            thread_root: msg_like.and_then(|content| content.thread_root.clone()),
960ac0721079atagen2026-09-25 00:24 UTC118
            thread_summary: msg_like.and_then(thread_summary),
960ac0721079atagen2026-09-25 00:24 UTC119
            reactions: reactions(event, self.current_user_id),
960ac0721079atagen2026-09-25 00:24 UTC120
            read_by,
960ac0721079atagen2026-09-25 00:24 UTC121
            editable: event.is_editable(),
960ac0721079atagen2026-09-25 00:24 UTC122
        }
960ac0721079atagen2026-09-25 00:24 UTC123
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC124
}
96fdf90aacd6atagen2026-09-08 08:24 UTC125
96fdf90aacd6atagen2026-09-08 08:24 UTC126
pub(crate) fn normalise_timeline<'a>(
96fdf90aacd6atagen2026-09-08 08:24 UTC127
    items: impl Iterator<Item = &'a Arc<SdkTimelineItem>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC128
    current_user_id: Option<&UserId>,
96fdf90aacd6atagen2026-09-08 08:24 UTC129
    scope: &MediaScope,
96fdf90aacd6atagen2026-09-08 08:24 UTC130
) -> NormalisedTimeline {
960ac0721079atagen2026-09-25 00:24 UTC131
    let mut normaliser = Normaliser {
960ac0721079atagen2026-09-25 00:24 UTC132
        current_user_id,
960ac0721079atagen2026-09-25 00:24 UTC133
        scope,
96fdf90aacd6atagen2026-09-08 08:24 UTC134
        media: Vec::new(),
960ac0721079atagen2026-09-25 00:24 UTC135
        missing_replies: Vec::new(),
96fdf90aacd6atagen2026-09-08 08:24 UTC136
    };
960ac0721079atagen2026-09-25 00:24 UTC137
    let items = items.map(|item| normaliser.item(item)).collect();
960ac0721079atagen2026-09-25 00:24 UTC138
    NormalisedTimeline {
960ac0721079atagen2026-09-25 00:24 UTC139
        items,
960ac0721079atagen2026-09-25 00:24 UTC140
        media: normaliser.media,
960ac0721079atagen2026-09-25 00:24 UTC141
        missing_replies: normaliser.missing_replies,
96fdf90aacd6atagen2026-09-08 08:24 UTC142
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC143
}
96fdf90aacd6atagen2026-09-08 08:24 UTC144
96fdf90aacd6atagen2026-09-08 08:24 UTC145
pub(crate) fn normalise_timeline_diffs(
96fdf90aacd6atagen2026-09-08 08:24 UTC146
    diffs: Vec<VectorDiff<Arc<SdkTimelineItem>>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC147
    current_user_id: Option<&UserId>,
96fdf90aacd6atagen2026-09-08 08:24 UTC148
    scope: &MediaScope,
96fdf90aacd6atagen2026-09-08 08:24 UTC149
) -> NormalisedTimelineDiffs {
960ac0721079atagen2026-09-25 00:24 UTC150
    let mut normaliser = Normaliser {
960ac0721079atagen2026-09-25 00:24 UTC151
        current_user_id,
960ac0721079atagen2026-09-25 00:24 UTC152
        scope,
960ac0721079atagen2026-09-25 00:24 UTC153
        media: Vec::new(),
960ac0721079atagen2026-09-25 00:24 UTC154
        missing_replies: Vec::new(),
960ac0721079atagen2026-09-25 00:24 UTC155
    };
96fdf90aacd6atagen2026-09-08 08:24 UTC156
    let diffs = diffs
96fdf90aacd6atagen2026-09-08 08:24 UTC157
        .into_iter()
960ac0721079atagen2026-09-25 00:24 UTC158
        .map(|diff| match diff.map(|item| normaliser.item(&item)) {
960ac0721079atagen2026-09-25 00:24 UTC159
            VectorDiff::Append { values } => TimelineDiff::Append {
960ac0721079atagen2026-09-25 00:24 UTC160
                values: values.into_iter().collect(),
960ac0721079atagen2026-09-25 00:24 UTC161
            },
960ac0721079atagen2026-09-25 00:24 UTC162
            VectorDiff::Clear => TimelineDiff::Clear,
960ac0721079atagen2026-09-25 00:24 UTC163
            VectorDiff::PushFront { value } => TimelineDiff::PushFront { value },
960ac0721079atagen2026-09-25 00:24 UTC164
            VectorDiff::PushBack { value } => TimelineDiff::PushBack { value },
960ac0721079atagen2026-09-25 00:24 UTC165
            VectorDiff::PopFront => TimelineDiff::PopFront,
960ac0721079atagen2026-09-25 00:24 UTC166
            VectorDiff::PopBack => TimelineDiff::PopBack,
960ac0721079atagen2026-09-25 00:24 UTC167
            VectorDiff::Insert { index, value } => TimelineDiff::Insert { index, value },
960ac0721079atagen2026-09-25 00:24 UTC168
            VectorDiff::Set { index, value } => TimelineDiff::Set { index, value },
960ac0721079atagen2026-09-25 00:24 UTC169
            VectorDiff::Remove { index } => TimelineDiff::Remove { index },
960ac0721079atagen2026-09-25 00:24 UTC170
            VectorDiff::Truncate { length } => TimelineDiff::Truncate { length },
960ac0721079atagen2026-09-25 00:24 UTC171
            VectorDiff::Reset { values } => TimelineDiff::Reset {
960ac0721079atagen2026-09-25 00:24 UTC172
                values: values.into_iter().collect(),
960ac0721079atagen2026-09-25 00:24 UTC173
            },
96fdf90aacd6atagen2026-09-08 08:24 UTC174
        })
96fdf90aacd6atagen2026-09-08 08:24 UTC175
        .collect();
960ac0721079atagen2026-09-25 00:24 UTC176
    NormalisedTimelineDiffs {
960ac0721079atagen2026-09-25 00:24 UTC177
        diffs,
960ac0721079atagen2026-09-25 00:24 UTC178
        media: normaliser.media,
960ac0721079atagen2026-09-25 00:24 UTC179
        missing_replies: normaliser.missing_replies,
960ac0721079atagen2026-09-25 00:24 UTC180
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC181
}
96fdf90aacd6atagen2026-09-08 08:24 UTC182
960ac0721079atagen2026-09-25 00:24 UTC183
/// `media_identity` is `None` for embedded reply content, which never gets fetchable media of its own
960ac0721079atagen2026-09-25 00:24 UTC184
fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> TimelineContent {
960ac0721079atagen2026-09-25 00:24 UTC185
    match content {
960ac0721079atagen2026-09-25 00:24 UTC186
        TimelineItemContent::MsgLike(content) => msg_like(content, media_identity),
960ac0721079atagen2026-09-25 00:24 UTC187
        TimelineItemContent::MembershipChange(change) => TimelineContent::Membership {
960ac0721079atagen2026-09-25 00:24 UTC188
            user_id: change.user_id().to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC189
            display_name: change.display_name(),
960ac0721079atagen2026-09-25 00:24 UTC190
            change: change
960ac0721079atagen2026-09-25 00:24 UTC191
                .change()
960ac0721079atagen2026-09-25 00:24 UTC192
                .map_or(MembershipChange::Other, membership_change),
960ac0721079atagen2026-09-25 00:24 UTC193
        },
960ac0721079atagen2026-09-25 00:24 UTC194
        TimelineItemContent::ProfileChange(change) => TimelineContent::Profile {
960ac0721079atagen2026-09-25 00:24 UTC195
            user_id: change.user_id().to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC196
            display_name: change.displayname_change().map(|change| Change {
960ac0721079atagen2026-09-25 00:24 UTC197
                old: change.old.clone(),
960ac0721079atagen2026-09-25 00:24 UTC198
                new: change.new.clone(),
960ac0721079atagen2026-09-25 00:24 UTC199
            }),
960ac0721079atagen2026-09-25 00:24 UTC200
            avatar: change.avatar_url_change().map(|change| Change {
960ac0721079atagen2026-09-25 00:24 UTC201
                old: change.old.clone(),
960ac0721079atagen2026-09-25 00:24 UTC202
                new: change.new.clone(),
960ac0721079atagen2026-09-25 00:24 UTC203
            }),
960ac0721079atagen2026-09-25 00:24 UTC204
        },
960ac0721079atagen2026-09-25 00:24 UTC205
        TimelineItemContent::OtherState(state) => match state.content() {
5a52012c9b1datagen2026-09-24 23:45 UTC206
            AnyOtherStateEventContentChange::RoomName(change) => TimelineContent::RoomName(
5a52012c9b1datagen2026-09-24 23:45 UTC207
                original(change).and_then(|content| non_empty(&content.name)),
5a52012c9b1datagen2026-09-24 23:45 UTC208
            ),
5a52012c9b1datagen2026-09-24 23:45 UTC209
            AnyOtherStateEventContentChange::RoomTopic(change) => TimelineContent::RoomTopic(
5a52012c9b1datagen2026-09-24 23:45 UTC210
                original(change).and_then(|content| non_empty(&content.topic)),
5a52012c9b1datagen2026-09-24 23:45 UTC211
            ),
960ac0721079atagen2026-09-25 00:24 UTC212
            AnyOtherStateEventContentChange::RoomAvatar(change) => TimelineContent::RoomAvatar(
960ac0721079atagen2026-09-25 00:24 UTC213
                original(change).and_then(|content| content.url.clone()),
960ac0721079atagen2026-09-25 00:24 UTC214
            ),
960ac0721079atagen2026-09-25 00:24 UTC215
            AnyOtherStateEventContentChange::RoomEncryption(_) => {
960ac0721079atagen2026-09-25 00:24 UTC216
                TimelineContent::EncryptionEnabled
960ac0721079atagen2026-09-25 00:24 UTC217
            }
960ac0721079atagen2026-09-25 00:24 UTC218
            AnyOtherStateEventContentChange::RoomCreate(_) => TimelineContent::RoomCreated,
960ac0721079atagen2026-09-25 00:24 UTC219
            AnyOtherStateEventContentChange::RoomTombstone(change) => match original(change) {
960ac0721079atagen2026-09-25 00:24 UTC220
                Some(content) => TimelineContent::RoomUpgraded {
960ac0721079atagen2026-09-25 00:24 UTC221
                    successor: content.replacement_room.clone(),
960ac0721079atagen2026-09-25 00:24 UTC222
                },
960ac0721079atagen2026-09-25 00:24 UTC223
                None => other_state(state.content().event_type().to_string(), state.state_key()),
960ac0721079atagen2026-09-25 00:24 UTC224
            },
960ac0721079atagen2026-09-25 00:24 UTC225
            other => other_state(other.event_type().to_string(), state.state_key()),
960ac0721079atagen2026-09-25 00:24 UTC226
        },
960ac0721079atagen2026-09-25 00:24 UTC227
        TimelineItemContent::FailedToParseMessageLike { event_type, .. } => {
960ac0721079atagen2026-09-25 00:24 UTC228
            TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC229
                event_type: event_type.to_string(),
960ac0721079atagen2026-09-25 00:24 UTC230
            }
960ac0721079atagen2026-09-25 00:24 UTC231
        }
960ac0721079atagen2026-09-25 00:24 UTC232
        TimelineItemContent::FailedToParseState {
960ac0721079atagen2026-09-25 00:24 UTC233
            event_type,
960ac0721079atagen2026-09-25 00:24 UTC234
            state_key,
960ac0721079atagen2026-09-25 00:24 UTC235
            ..
960ac0721079atagen2026-09-25 00:24 UTC236
        } => other_state(event_type.to_string(), state_key),
960ac0721079atagen2026-09-25 00:24 UTC237
        TimelineItemContent::CallInvite => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC238
            event_type: "m.call.invite".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC239
        },
960ac0721079atagen2026-09-25 00:24 UTC240
        TimelineItemContent::RtcNotification { .. } => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC241
            event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC242
        },
960ac0721079atagen2026-09-25 00:24 UTC243
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC244
}
96fdf90aacd6atagen2026-09-08 08:24 UTC245
5a52012c9b1datagen2026-09-24 23:45 UTC246
/// a cleared name or topic arrives as an empty string
5a52012c9b1datagen2026-09-24 23:45 UTC247
fn non_empty(value: &str) -> Option<String> {
5a52012c9b1datagen2026-09-24 23:45 UTC248
    (!value.is_empty()).then(|| value.to_owned())
5a52012c9b1datagen2026-09-24 23:45 UTC249
}
5a52012c9b1datagen2026-09-24 23:45 UTC250
960ac0721079atagen2026-09-25 00:24 UTC251
fn other_state(event_type: String, state_key: &str) -> TimelineContent {
960ac0721079atagen2026-09-25 00:24 UTC252
    TimelineContent::OtherState {
960ac0721079atagen2026-09-25 00:24 UTC253
        event_type,
960ac0721079atagen2026-09-25 00:24 UTC254
        state_key: state_key.to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC255
    }
960ac0721079atagen2026-09-25 00:24 UTC256
}
960ac0721079atagen2026-09-25 00:24 UTC257
960ac0721079atagen2026-09-25 00:24 UTC258
fn original<C>(change: &StateEventContentChange<C>) -> Option<&C>
960ac0721079atagen2026-09-25 00:24 UTC259
where
960ac0721079atagen2026-09-25 00:24 UTC260
    C: ruma::events::StaticStateEventContent + ruma::events::RedactContent,
960ac0721079atagen2026-09-25 00:24 UTC261
{
960ac0721079atagen2026-09-25 00:24 UTC262
    match change {
960ac0721079atagen2026-09-25 00:24 UTC263
        StateEventContentChange::Original { content, .. } => Some(content),
960ac0721079atagen2026-09-25 00:24 UTC264
        StateEventContentChange::Redacted(_) => None,
960ac0721079atagen2026-09-25 00:24 UTC265
    }
960ac0721079atagen2026-09-25 00:24 UTC266
}
960ac0721079atagen2026-09-25 00:24 UTC267
960ac0721079atagen2026-09-25 00:24 UTC268
fn msg_like(content: &MsgLikeContent, media_identity: Option<&str>) -> TimelineContent {
960ac0721079atagen2026-09-25 00:24 UTC269
    match &content.kind {
960ac0721079atagen2026-09-25 00:24 UTC270
        MsgLikeKind::Message(message) => TimelineContent::Message(Message {
960ac0721079atagen2026-09-25 00:24 UTC271
            kind: message_kind(message.msgtype()),
960ac0721079atagen2026-09-25 00:24 UTC272
            body: message.body().to_owned(),
5a52012c9b1datagen2026-09-24 23:45 UTC273
            geo_uri: match message.msgtype() {
5a52012c9b1datagen2026-09-24 23:45 UTC274
                MessageType::Location(location) => Some(location.geo_uri.clone()),
5a52012c9b1datagen2026-09-24 23:45 UTC275
                _ => None,
5a52012c9b1datagen2026-09-24 23:45 UTC276
            },
960ac0721079atagen2026-09-25 00:24 UTC277
            formatted_body: formatted_body(message.msgtype()),
960ac0721079atagen2026-09-25 00:24 UTC278
            mentions: message.mentions().map(|mentions| MessageMentions {
960ac0721079atagen2026-09-25 00:24 UTC279
                user_ids: mentions.user_ids.iter().cloned().collect(),
960ac0721079atagen2026-09-25 00:24 UTC280
                room: mentions.room,
960ac0721079atagen2026-09-25 00:24 UTC281
            }),
960ac0721079atagen2026-09-25 00:24 UTC282
            edited: message.is_edited(),
960ac0721079atagen2026-09-25 00:24 UTC283
            media: media_identity
960ac0721079atagen2026-09-25 00:24 UTC284
                .and_then(|identity| media_from_message(identity, message.msgtype())),
960ac0721079atagen2026-09-25 00:24 UTC285
        }),
960ac0721079atagen2026-09-25 00:24 UTC286
        MsgLikeKind::Sticker(sticker) => {
960ac0721079atagen2026-09-25 00:24 UTC287
            let sticker = sticker.content();
960ac0721079atagen2026-09-25 00:24 UTC288
            match media_identity {
960ac0721079atagen2026-09-25 00:24 UTC289
                Some(identity) => TimelineContent::Sticker {
960ac0721079atagen2026-09-25 00:24 UTC290
                    body: sticker.body.clone(),
960ac0721079atagen2026-09-25 00:24 UTC291
                    media: media_descriptor_for_sticker(identity, sticker),
960ac0721079atagen2026-09-25 00:24 UTC292
                },
960ac0721079atagen2026-09-25 00:24 UTC293
                None => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC294
                    event_type: "m.sticker".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC295
                },
96fdf90aacd6atagen2026-09-08 08:24 UTC296
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC297
        }
960ac0721079atagen2026-09-25 00:24 UTC298
        MsgLikeKind::Poll(poll) => TimelineContent::Poll {
960ac0721079atagen2026-09-25 00:24 UTC299
            question: poll.results().question,
960ac0721079atagen2026-09-25 00:24 UTC300
        },
960ac0721079atagen2026-09-25 00:24 UTC301
        MsgLikeKind::Redacted => TimelineContent::Redacted,
960ac0721079atagen2026-09-25 00:24 UTC302
        MsgLikeKind::UnableToDecrypt(encrypted) => {
960ac0721079atagen2026-09-25 00:24 UTC303
            TimelineContent::UnableToDecrypt(match encrypted {
960ac0721079atagen2026-09-25 00:24 UTC304
                EncryptedMessage::MegolmV1AesSha2 { cause, .. } => utd_cause(*cause),
960ac0721079atagen2026-09-25 00:24 UTC305
                _ => UtdCause::Unknown,
960ac0721079atagen2026-09-25 00:24 UTC306
            })
96fdf90aacd6atagen2026-09-08 08:24 UTC307
        }
960ac0721079atagen2026-09-25 00:24 UTC308
        MsgLikeKind::Other(other) => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC309
            event_type: other.event_type().to_string(),
960ac0721079atagen2026-09-25 00:24 UTC310
        },
960ac0721079atagen2026-09-25 00:24 UTC311
        MsgLikeKind::LiveLocation(_) => TimelineContent::Other {
960ac0721079atagen2026-09-25 00:24 UTC312
            event_type: "m.beacon_info".to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC313
        },
96fdf90aacd6atagen2026-09-08 08:24 UTC314
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC315
}
96fdf90aacd6atagen2026-09-08 08:24 UTC316
5a52012c9b1datagen2026-09-24 23:45 UTC317
pub(crate) fn message_kind(message: &MessageType) -> MessageKind {
960ac0721079atagen2026-09-25 00:24 UTC318
    match message {
960ac0721079atagen2026-09-25 00:24 UTC319
        MessageType::Text(_) => MessageKind::Text,
960ac0721079atagen2026-09-25 00:24 UTC320
        MessageType::Emote(_) => MessageKind::Emote,
960ac0721079atagen2026-09-25 00:24 UTC321
        MessageType::Notice(_) => MessageKind::Notice,
960ac0721079atagen2026-09-25 00:24 UTC322
        MessageType::ServerNotice(_) => MessageKind::ServerNotice,
960ac0721079atagen2026-09-25 00:24 UTC323
        MessageType::Image(_) => MessageKind::Image,
960ac0721079atagen2026-09-25 00:24 UTC324
        MessageType::Video(_) => MessageKind::Video,
960ac0721079atagen2026-09-25 00:24 UTC325
        MessageType::Audio(_) => MessageKind::Audio,
960ac0721079atagen2026-09-25 00:24 UTC326
        MessageType::File(_) => MessageKind::File,
960ac0721079atagen2026-09-25 00:24 UTC327
        MessageType::Location(_) => MessageKind::Location,
960ac0721079atagen2026-09-25 00:24 UTC328
        MessageType::VerificationRequest(_) => MessageKind::VerificationRequest,
960ac0721079atagen2026-09-25 00:24 UTC329
        other => MessageKind::Other(other.msgtype().to_owned()),
960ac0721079atagen2026-09-25 00:24 UTC330
    }
960ac0721079atagen2026-09-25 00:24 UTC331
}
960ac0721079atagen2026-09-25 00:24 UTC332
960ac0721079atagen2026-09-25 00:24 UTC333
const fn membership_change(change: SdkMembershipChange) -> MembershipChange {
960ac0721079atagen2026-09-25 00:24 UTC334
    match change {
960ac0721079atagen2026-09-25 00:24 UTC335
        SdkMembershipChange::Joined => MembershipChange::Joined,
960ac0721079atagen2026-09-25 00:24 UTC336
        SdkMembershipChange::Left => MembershipChange::Left,
960ac0721079atagen2026-09-25 00:24 UTC337
        SdkMembershipChange::Banned => MembershipChange::Banned,
960ac0721079atagen2026-09-25 00:24 UTC338
        SdkMembershipChange::Unbanned => MembershipChange::Unbanned,
960ac0721079atagen2026-09-25 00:24 UTC339
        SdkMembershipChange::Kicked => MembershipChange::Kicked,
960ac0721079atagen2026-09-25 00:24 UTC340
        SdkMembershipChange::KickedAndBanned => MembershipChange::KickedAndBanned,
960ac0721079atagen2026-09-25 00:24 UTC341
        SdkMembershipChange::Invited => MembershipChange::Invited,
960ac0721079atagen2026-09-25 00:24 UTC342
        SdkMembershipChange::InvitationAccepted => MembershipChange::InvitationAccepted,
960ac0721079atagen2026-09-25 00:24 UTC343
        SdkMembershipChange::InvitationRejected => MembershipChange::InvitationRejected,
960ac0721079atagen2026-09-25 00:24 UTC344
        SdkMembershipChange::InvitationRevoked => MembershipChange::InvitationRevoked,
960ac0721079atagen2026-09-25 00:24 UTC345
        SdkMembershipChange::Knocked => MembershipChange::Knocked,
960ac0721079atagen2026-09-25 00:24 UTC346
        SdkMembershipChange::KnockAccepted => MembershipChange::KnockAccepted,
960ac0721079atagen2026-09-25 00:24 UTC347
        SdkMembershipChange::KnockRetracted => MembershipChange::KnockRetracted,
960ac0721079atagen2026-09-25 00:24 UTC348
        SdkMembershipChange::KnockDenied => MembershipChange::KnockDenied,
960ac0721079atagen2026-09-25 00:24 UTC349
        SdkMembershipChange::None
960ac0721079atagen2026-09-25 00:24 UTC350
        | SdkMembershipChange::Error
960ac0721079atagen2026-09-25 00:24 UTC351
        | SdkMembershipChange::NotImplemented => MembershipChange::Other,
960ac0721079atagen2026-09-25 00:24 UTC352
    }
960ac0721079atagen2026-09-25 00:24 UTC353
}
960ac0721079atagen2026-09-25 00:24 UTC354
960ac0721079atagen2026-09-25 00:24 UTC355
const fn utd_cause(cause: SdkUtdCause) -> UtdCause {
960ac0721079atagen2026-09-25 00:24 UTC356
    match cause {
960ac0721079atagen2026-09-25 00:24 UTC357
        SdkUtdCause::Unknown => UtdCause::Unknown,
960ac0721079atagen2026-09-25 00:24 UTC358
        SdkUtdCause::SentBeforeWeJoined => UtdCause::SentBeforeWeJoined,
960ac0721079atagen2026-09-25 00:24 UTC359
        SdkUtdCause::VerificationViolation => UtdCause::VerificationViolation,
960ac0721079atagen2026-09-25 00:24 UTC360
        SdkUtdCause::UnsignedDevice => UtdCause::UnsignedDevice,
960ac0721079atagen2026-09-25 00:24 UTC361
        SdkUtdCause::UnknownDevice => UtdCause::UnknownDevice,
960ac0721079atagen2026-09-25 00:24 UTC362
        SdkUtdCause::HistoricalMessageAndBackupIsDisabled => {
960ac0721079atagen2026-09-25 00:24 UTC363
            UtdCause::HistoricalMessageAndBackupIsDisabled
960ac0721079atagen2026-09-25 00:24 UTC364
        }
960ac0721079atagen2026-09-25 00:24 UTC365
        SdkUtdCause::WithheldForUnverifiedOrInsecureDevice => {
960ac0721079atagen2026-09-25 00:24 UTC366
            UtdCause::WithheldForUnverifiedOrInsecureDevice
960ac0721079atagen2026-09-25 00:24 UTC367
        }
960ac0721079atagen2026-09-25 00:24 UTC368
        SdkUtdCause::WithheldBySender => UtdCause::WithheldBySender,
960ac0721079atagen2026-09-25 00:24 UTC369
        SdkUtdCause::HistoricalMessageAndDeviceIsUnverified => {
960ac0721079atagen2026-09-25 00:24 UTC370
            UtdCause::HistoricalMessageAndDeviceIsUnverified
960ac0721079atagen2026-09-25 00:24 UTC371
        }
960ac0721079atagen2026-09-25 00:24 UTC372
    }
960ac0721079atagen2026-09-25 00:24 UTC373
}
960ac0721079atagen2026-09-25 00:24 UTC374
960ac0721079atagen2026-09-25 00:24 UTC375
fn sender(user_id: &UserId, profile: &TimelineDetails<Profile>) -> Sender {
960ac0721079atagen2026-09-25 00:24 UTC376
    let profile = match profile {
960ac0721079atagen2026-09-25 00:24 UTC377
        TimelineDetails::Ready(profile) => Some(profile),
96fdf90aacd6atagen2026-09-08 08:24 UTC378
        _ => None,
96fdf90aacd6atagen2026-09-08 08:24 UTC379
    };
960ac0721079atagen2026-09-25 00:24 UTC380
    Sender {
960ac0721079atagen2026-09-25 00:24 UTC381
        user_id: user_id.to_owned(),
960ac0721079atagen2026-09-25 00:24 UTC382
        display_name: profile.and_then(|profile| profile.display_name.clone()),
960ac0721079atagen2026-09-25 00:24 UTC383
        avatar: profile.and_then(|profile| profile.avatar_url.clone()),
960ac0721079atagen2026-09-25 00:24 UTC384
        name_ambiguous: profile.is_some_and(|profile| profile.display_name_ambiguous),
960ac0721079atagen2026-09-25 00:24 UTC385
    }
960ac0721079atagen2026-09-25 00:24 UTC386
}
960ac0721079atagen2026-09-25 00:24 UTC387
960ac0721079atagen2026-09-25 00:24 UTC388
fn send_state(state: Option<&EventSendState>) -> SendState {
960ac0721079atagen2026-09-25 00:24 UTC389
    match state {
960ac0721079atagen2026-09-25 00:24 UTC390
        None | Some(EventSendState::Sent { .. }) => SendState::Sent,
5a52012c9b1datagen2026-09-24 23:45 UTC391
        Some(EventSendState::NotSentYet { .. }) => SendState::Sending,
5a52012c9b1datagen2026-09-24 23:45 UTC392
        // recoverable means the sdk parks the queue and we unpark it, nothing for the user to do
960ac0721079atagen2026-09-25 00:24 UTC393
        Some(EventSendState::SendingFailed { is_recoverable, .. }) => SendState::Failed {
5a52012c9b1datagen2026-09-24 23:45 UTC394
            will_retry: *is_recoverable,
960ac0721079atagen2026-09-25 00:24 UTC395
        },
960ac0721079atagen2026-09-25 00:24 UTC396
    }
960ac0721079atagen2026-09-25 00:24 UTC397
}
960ac0721079atagen2026-09-25 00:24 UTC398
960ac0721079atagen2026-09-25 00:24 UTC399
fn thread_summary(content: &MsgLikeContent) -> Option<ThreadSummary> {
960ac0721079atagen2026-09-25 00:24 UTC400
    content.thread_summary.as_ref().map(|summary| {
960ac0721079atagen2026-09-25 00:24 UTC401
        let latest_event = match &summary.latest_event {
960ac0721079atagen2026-09-25 00:24 UTC402
            TimelineDetails::Ready(event) => Some(event.as_ref()),
960ac0721079atagen2026-09-25 00:24 UTC403
            _ => None,
960ac0721079atagen2026-09-25 00:24 UTC404
        };
960ac0721079atagen2026-09-25 00:24 UTC405
        ThreadSummary {
960ac0721079atagen2026-09-25 00:24 UTC406
            reply_count: summary.num_replies,
960ac0721079atagen2026-09-25 00:24 UTC407
            latest_event_id: latest_event.and_then(|event| match &event.identifier {
960ac0721079atagen2026-09-25 00:24 UTC408
                TimelineEventItemId::EventId(event_id) => Some(event_id.clone()),
960ac0721079atagen2026-09-25 00:24 UTC409
                TimelineEventItemId::TransactionId(_) => None,
96fdf90aacd6atagen2026-09-08 08:24 UTC410
            }),
960ac0721079atagen2026-09-25 00:24 UTC411
            latest_event_timestamp_ms: latest_event.map(|event| event.timestamp.get().into()),
960ac0721079atagen2026-09-25 00:24 UTC412
        }
960ac0721079atagen2026-09-25 00:24 UTC413
    })
960ac0721079atagen2026-09-25 00:24 UTC414
}
960ac0721079atagen2026-09-25 00:24 UTC415
960ac0721079atagen2026-09-25 00:24 UTC416
fn reactions(event: &EventTimelineItem, current_user_id: Option<&UserId>) -> Vec<Reaction> {
960ac0721079atagen2026-09-25 00:24 UTC417
    event
96fdf90aacd6atagen2026-09-08 08:24 UTC418
        .content()
96fdf90aacd6atagen2026-09-08 08:24 UTC419
        .reactions()
96fdf90aacd6atagen2026-09-08 08:24 UTC420
        .into_iter()
96fdf90aacd6atagen2026-09-08 08:24 UTC421
        .flat_map(|reactions| reactions.iter())
96fdf90aacd6atagen2026-09-08 08:24 UTC422
        .map(|(key, senders)| Reaction {
96fdf90aacd6atagen2026-09-08 08:24 UTC423
            key: key.clone(),
96fdf90aacd6atagen2026-09-08 08:24 UTC424
            count: senders.len(),
96fdf90aacd6atagen2026-09-08 08:24 UTC425
            own: current_user_id.is_some_and(|user_id| senders.contains_key(user_id)),
96fdf90aacd6atagen2026-09-08 08:24 UTC426
            senders: senders.keys().cloned().collect(),
96fdf90aacd6atagen2026-09-08 08:24 UTC427
        })
960ac0721079atagen2026-09-25 00:24 UTC428
        .collect()
96fdf90aacd6atagen2026-09-08 08:24 UTC429
}
96fdf90aacd6atagen2026-09-08 08:24 UTC430
96fdf90aacd6atagen2026-09-08 08:24 UTC431
fn formatted_body(message: &MessageType) -> Option<String> {
96fdf90aacd6atagen2026-09-08 08:24 UTC432
    let formatted = match message {
96fdf90aacd6atagen2026-09-08 08:24 UTC433
        MessageType::Text(content) => content.formatted.as_ref(),
96fdf90aacd6atagen2026-09-08 08:24 UTC434
        MessageType::Emote(content) => content.formatted.as_ref(),
96fdf90aacd6atagen2026-09-08 08:24 UTC435
        MessageType::Notice(content) => content.formatted.as_ref(),
96fdf90aacd6atagen2026-09-08 08:24 UTC436
        MessageType::Audio(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC437
        MessageType::File(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC438
        MessageType::Image(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC439
        MessageType::Video(content) => content.formatted_caption(),
96fdf90aacd6atagen2026-09-08 08:24 UTC440
        _ => None,
96fdf90aacd6atagen2026-09-08 08:24 UTC441
    }?;
96fdf90aacd6atagen2026-09-08 08:24 UTC442
    (formatted.format == MessageFormat::Html).then(|| formatted.body.clone())
96fdf90aacd6atagen2026-09-08 08:24 UTC443
}