atagen

macltok

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

Blame: src/normalise/context.rs

cb7b3362e1ceatagen2026-10-25 09:08 UTC1
use std::collections::HashMap;
cb7b3362e1ceatagen2026-10-25 09:08 UTC2
cb7b3362e1ceatagen2026-10-25 09:08 UTC3
use ruma::{MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId};
cb7b3362e1ceatagen2026-10-25 09:08 UTC4
use serde_json::Value;
cb7b3362e1ceatagen2026-10-25 09:08 UTC5
cb7b3362e1ceatagen2026-10-25 09:08 UTC6
use super::{media::MediaRegistration, observed::normalise_observed};
cb7b3362e1ceatagen2026-10-25 09:08 UTC7
use crate::{MediaId, Reaction, ReplyDetails, Sender, TimelineEvent, TimelineItem};
cb7b3362e1ceatagen2026-10-25 09:08 UTC8
cb7b3362e1ceatagen2026-10-25 09:08 UTC9
pub(crate) struct ContextPage {
cb7b3362e1ceatagen2026-10-25 09:08 UTC10
    /// each with the index of the raw event it came from
cb7b3362e1ceatagen2026-10-25 09:08 UTC11
    pub(crate) items: Vec<(usize, TimelineItem)>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC12
    pub(crate) media: Vec<MediaRegistration>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC13
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC14
cb7b3362e1ceatagen2026-10-25 09:08 UTC15
struct Annotation<'a> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC16
    key: &'a str,
cb7b3362e1ceatagen2026-10-25 09:08 UTC17
    sender: &'a str,
cb7b3362e1ceatagen2026-10-25 09:08 UTC18
    shortcode: Option<&'a str>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC19
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC20
cb7b3362e1ceatagen2026-10-25 09:08 UTC21
/// raw events in timeline order, shown the way the live room timeline shows them. edits and
cb7b3362e1ceatagen2026-10-25 09:08 UTC22
/// reactions fold into their targets within the page, and redactions, unmatched relations
1f875b103b27atagen2026-10-25 10:44 UTC23
/// and thread replies go. `keep` is never dropped. `media_prefix` keeps one owner's media ids
1f875b103b27atagen2026-10-25 10:44 UTC24
/// apart from another's for the same event
cb7b3362e1ceatagen2026-10-25 09:08 UTC25
pub(crate) fn normalise_context(
cb7b3362e1ceatagen2026-10-25 09:08 UTC26
    values: &[Value],
cb7b3362e1ceatagen2026-10-25 09:08 UTC27
    keep: Option<usize>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC28
    current_user_id: Option<&UserId>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC29
    room_id: &RoomId,
1f875b103b27atagen2026-10-25 10:44 UTC30
    media_prefix: &str,
cb7b3362e1ceatagen2026-10-25 09:08 UTC31
) -> ContextPage {
cb7b3362e1ceatagen2026-10-25 09:08 UTC32
    let mut edits: HashMap<&str, Vec<&Value>> = HashMap::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC33
    let mut annotations: HashMap<&str, Vec<Annotation<'_>>> = HashMap::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC34
    let mut shown = Vec::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC35
    for (index, value) in values.iter().enumerate() {
cb7b3362e1ceatagen2026-10-25 09:08 UTC36
        let str_at = |pointer: &str| value.pointer(pointer).and_then(Value::as_str);
cb7b3362e1ceatagen2026-10-25 09:08 UTC37
        let target = str_at("/content/m.relates_to/event_id");
cb7b3362e1ceatagen2026-10-25 09:08 UTC38
        if Some(index) == keep {
cb7b3362e1ceatagen2026-10-25 09:08 UTC39
            shown.push(index);
cb7b3362e1ceatagen2026-10-25 09:08 UTC40
            continue;
cb7b3362e1ceatagen2026-10-25 09:08 UTC41
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC42
        match (
cb7b3362e1ceatagen2026-10-25 09:08 UTC43
            str_at("/type"),
cb7b3362e1ceatagen2026-10-25 09:08 UTC44
            str_at("/content/m.relates_to/rel_type"),
cb7b3362e1ceatagen2026-10-25 09:08 UTC45
            target,
cb7b3362e1ceatagen2026-10-25 09:08 UTC46
        ) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC47
            (Some("m.room.redaction"), ..) => {}
cb7b3362e1ceatagen2026-10-25 09:08 UTC48
            (Some("m.reaction"), Some("m.annotation"), Some(target)) => {
cb7b3362e1ceatagen2026-10-25 09:08 UTC49
                if let (Some(key), Some(sender)) =
cb7b3362e1ceatagen2026-10-25 09:08 UTC50
                    (str_at("/content/m.relates_to/key"), str_at("/sender"))
cb7b3362e1ceatagen2026-10-25 09:08 UTC51
                {
cb7b3362e1ceatagen2026-10-25 09:08 UTC52
                    annotations.entry(target).or_default().push(Annotation {
cb7b3362e1ceatagen2026-10-25 09:08 UTC53
                        key,
cb7b3362e1ceatagen2026-10-25 09:08 UTC54
                        sender,
cb7b3362e1ceatagen2026-10-25 09:08 UTC55
                        shortcode: str_at("/content/com.beeper.reaction.shortcode")
cb7b3362e1ceatagen2026-10-25 09:08 UTC56
                            .or_else(|| str_at("/content/shortcode")),
cb7b3362e1ceatagen2026-10-25 09:08 UTC57
                    });
cb7b3362e1ceatagen2026-10-25 09:08 UTC58
                }
cb7b3362e1ceatagen2026-10-25 09:08 UTC59
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC60
            (Some("m.reaction"), ..) => {}
1f875b103b27atagen2026-10-25 10:44 UTC61
            // an edit still encrypted keeps its relation in the clear but has no new content
1f875b103b27atagen2026-10-25 10:44 UTC62
            (_, Some("m.replace"), Some(target)) => {
1f875b103b27atagen2026-10-25 10:44 UTC63
                if value.pointer("/content/m.new_content").is_some() {
1f875b103b27atagen2026-10-25 10:44 UTC64
                    edits.entry(target).or_default().push(value);
1f875b103b27atagen2026-10-25 10:44 UTC65
                }
1f875b103b27atagen2026-10-25 10:44 UTC66
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC67
            (_, Some("m.thread"), _) => {}
cb7b3362e1ceatagen2026-10-25 09:08 UTC68
            _ => shown.push(index),
cb7b3362e1ceatagen2026-10-25 09:08 UTC69
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC70
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC71
    let mut media = Vec::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC72
    let items = shown
cb7b3362e1ceatagen2026-10-25 09:08 UTC73
        .into_iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC74
        .filter_map(|index| {
cb7b3362e1ceatagen2026-10-25 09:08 UTC75
            let mut value = std::borrow::Cow::Borrowed(&values[index]);
cb7b3362e1ceatagen2026-10-25 09:08 UTC76
            let event_id = value.get("event_id")?.as_str()?.to_owned();
cb7b3362e1ceatagen2026-10-25 09:08 UTC77
            let latest_edit = edits.get(event_id.as_str()).and_then(|edits| {
cb7b3362e1ceatagen2026-10-25 09:08 UTC78
                edits
cb7b3362e1ceatagen2026-10-25 09:08 UTC79
                    .iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC80
                    .rev()
cb7b3362e1ceatagen2026-10-25 09:08 UTC81
                    .find(|edit| edit.get("sender") == value.get("sender"))
cb7b3362e1ceatagen2026-10-25 09:08 UTC82
            });
cb7b3362e1ceatagen2026-10-25 09:08 UTC83
            if let Some(edit) = latest_edit.copied()
cb7b3362e1ceatagen2026-10-25 09:08 UTC84
                && value.pointer("/unsigned/m.relations/m.replace").is_none()
cb7b3362e1ceatagen2026-10-25 09:08 UTC85
            {
cb7b3362e1ceatagen2026-10-25 09:08 UTC86
                let unsigned = value
cb7b3362e1ceatagen2026-10-25 09:08 UTC87
                    .to_mut()
cb7b3362e1ceatagen2026-10-25 09:08 UTC88
                    .as_object_mut()?
cb7b3362e1ceatagen2026-10-25 09:08 UTC89
                    .entry("unsigned")
cb7b3362e1ceatagen2026-10-25 09:08 UTC90
                    .or_insert_with(|| Value::Object(Default::default()));
cb7b3362e1ceatagen2026-10-25 09:08 UTC91
                let relations = unsigned
cb7b3362e1ceatagen2026-10-25 09:08 UTC92
                    .as_object_mut()?
cb7b3362e1ceatagen2026-10-25 09:08 UTC93
                    .entry("m.relations")
cb7b3362e1ceatagen2026-10-25 09:08 UTC94
                    .or_insert_with(|| Value::Object(Default::default()));
cb7b3362e1ceatagen2026-10-25 09:08 UTC95
                relations
cb7b3362e1ceatagen2026-10-25 09:08 UTC96
                    .as_object_mut()?
cb7b3362e1ceatagen2026-10-25 09:08 UTC97
                    .insert("m.replace".to_owned(), edit.clone());
cb7b3362e1ceatagen2026-10-25 09:08 UTC98
            }
1f875b103b27atagen2026-10-25 10:44 UTC99
            let identity = format!("{media_prefix}{room_id}\n{event_id}");
cb7b3362e1ceatagen2026-10-25 09:08 UTC100
            let (mut item, registrations) = normalise_observed(&value, current_user_id, &identity)?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC101
            media.extend(registrations);
cb7b3362e1ceatagen2026-10-25 09:08 UTC102
            if let (TimelineItem::Event(event), Some(annotations)) =
cb7b3362e1ceatagen2026-10-25 09:08 UTC103
                (&mut item, annotations.get(event_id.as_str()))
cb7b3362e1ceatagen2026-10-25 09:08 UTC104
            {
cb7b3362e1ceatagen2026-10-25 09:08 UTC105
                event.reactions = reactions(annotations, current_user_id);
cb7b3362e1ceatagen2026-10-25 09:08 UTC106
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC107
            Some((index, item))
cb7b3362e1ceatagen2026-10-25 09:08 UTC108
        })
cb7b3362e1ceatagen2026-10-25 09:08 UTC109
        .collect();
cb7b3362e1ceatagen2026-10-25 09:08 UTC110
    ContextPage { items, media }
cb7b3362e1ceatagen2026-10-25 09:08 UTC111
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC112
cb7b3362e1ceatagen2026-10-25 09:08 UTC113
fn reactions(annotations: &[Annotation<'_>], current_user_id: Option<&UserId>) -> Vec<Reaction> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC114
    let mut reactions: Vec<Reaction> = Vec::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC115
    for annotation in annotations {
cb7b3362e1ceatagen2026-10-25 09:08 UTC116
        let Ok(sender) = OwnedUserId::try_from(annotation.sender) else {
cb7b3362e1ceatagen2026-10-25 09:08 UTC117
            continue;
cb7b3362e1ceatagen2026-10-25 09:08 UTC118
        };
cb7b3362e1ceatagen2026-10-25 09:08 UTC119
        let index = match reactions.iter().position(|r| r.key == annotation.key) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC120
            Some(index) => index,
cb7b3362e1ceatagen2026-10-25 09:08 UTC121
            None => {
cb7b3362e1ceatagen2026-10-25 09:08 UTC122
                reactions.push(Reaction {
cb7b3362e1ceatagen2026-10-25 09:08 UTC123
                    key: annotation.key.to_owned(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC124
                    shortcode: None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC125
                    image: <&MxcUri>::from(annotation.key)
cb7b3362e1ceatagen2026-10-25 09:08 UTC126
                        .is_valid()
cb7b3362e1ceatagen2026-10-25 09:08 UTC127
                        .then(|| MediaId::plain(annotation.key)),
cb7b3362e1ceatagen2026-10-25 09:08 UTC128
                    count: 0,
cb7b3362e1ceatagen2026-10-25 09:08 UTC129
                    own: false,
cb7b3362e1ceatagen2026-10-25 09:08 UTC130
                    senders: Vec::new(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC131
                });
cb7b3362e1ceatagen2026-10-25 09:08 UTC132
                reactions.len() - 1
cb7b3362e1ceatagen2026-10-25 09:08 UTC133
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC134
        };
cb7b3362e1ceatagen2026-10-25 09:08 UTC135
        let reaction = &mut reactions[index];
cb7b3362e1ceatagen2026-10-25 09:08 UTC136
        if reaction.senders.contains(&sender) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC137
            continue;
cb7b3362e1ceatagen2026-10-25 09:08 UTC138
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC139
        reaction.own |= current_user_id == Some(sender.as_ref());
cb7b3362e1ceatagen2026-10-25 09:08 UTC140
        reaction.senders.push(sender);
cb7b3362e1ceatagen2026-10-25 09:08 UTC141
        reaction.count = reaction.senders.len();
cb7b3362e1ceatagen2026-10-25 09:08 UTC142
        if reaction.shortcode.is_none() {
cb7b3362e1ceatagen2026-10-25 09:08 UTC143
            reaction.shortcode = annotation.shortcode.map(str::to_owned);
cb7b3362e1ceatagen2026-10-25 09:08 UTC144
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC145
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC146
    reactions
cb7b3362e1ceatagen2026-10-25 09:08 UTC147
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC148
cb7b3362e1ceatagen2026-10-25 09:08 UTC149
/// the sender's member data, for each event and every reply quoted in it
cb7b3362e1ceatagen2026-10-25 09:08 UTC150
pub(crate) fn apply_profiles<'a>(
cb7b3362e1ceatagen2026-10-25 09:08 UTC151
    events: impl Iterator<Item = &'a mut TimelineEvent>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC152
    profiles: &HashMap<OwnedUserId, Sender>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC153
) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC154
    for event in events {
cb7b3362e1ceatagen2026-10-25 09:08 UTC155
        if let Some(sender) = profiles.get(&event.sender.user_id) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC156
            event.sender = sender.clone();
cb7b3362e1ceatagen2026-10-25 09:08 UTC157
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC158
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC159
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC160
cb7b3362e1ceatagen2026-10-25 09:08 UTC161
/// settles every reply, from `targets` or as unavailable
cb7b3362e1ceatagen2026-10-25 09:08 UTC162
pub(crate) fn resolve_replies<'a>(
cb7b3362e1ceatagen2026-10-25 09:08 UTC163
    events: impl Iterator<Item = &'a mut TimelineEvent>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC164
    targets: &HashMap<OwnedEventId, TimelineEvent>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC165
) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC166
    for event in events {
cb7b3362e1ceatagen2026-10-25 09:08 UTC167
        let Some(reply) = event.reply_to.as_mut() else {
cb7b3362e1ceatagen2026-10-25 09:08 UTC168
            continue;
cb7b3362e1ceatagen2026-10-25 09:08 UTC169
        };
cb7b3362e1ceatagen2026-10-25 09:08 UTC170
        if !matches!(reply.details, ReplyDetails::Pending) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC171
            continue;
cb7b3362e1ceatagen2026-10-25 09:08 UTC172
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC173
        reply.details = match targets.get(&reply.event_id) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC174
            Some(target) => ReplyDetails::Ready {
cb7b3362e1ceatagen2026-10-25 09:08 UTC175
                sender: target.sender.clone(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC176
                content: Box::new(target.content.clone()),
cb7b3362e1ceatagen2026-10-25 09:08 UTC177
                timestamp_ms: target.timestamp_ms,
cb7b3362e1ceatagen2026-10-25 09:08 UTC178
            },
cb7b3362e1ceatagen2026-10-25 09:08 UTC179
            None => ReplyDetails::Unavailable,
cb7b3362e1ceatagen2026-10-25 09:08 UTC180
        };
cb7b3362e1ceatagen2026-10-25 09:08 UTC181
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC182
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC183
cb7b3362e1ceatagen2026-10-25 09:08 UTC184
#[cfg(test)]
cb7b3362e1ceatagen2026-10-25 09:08 UTC185
mod tests {
cb7b3362e1ceatagen2026-10-25 09:08 UTC186
    use ruma::{room_id, user_id};
cb7b3362e1ceatagen2026-10-25 09:08 UTC187
    use serde_json::json;
cb7b3362e1ceatagen2026-10-25 09:08 UTC188
cb7b3362e1ceatagen2026-10-25 09:08 UTC189
    use super::*;
cb7b3362e1ceatagen2026-10-25 09:08 UTC190
    use crate::TimelineContent;
cb7b3362e1ceatagen2026-10-25 09:08 UTC191
cb7b3362e1ceatagen2026-10-25 09:08 UTC192
    fn message(id: &str, sender: &str, body: &str) -> Value {
cb7b3362e1ceatagen2026-10-25 09:08 UTC193
        json!({
cb7b3362e1ceatagen2026-10-25 09:08 UTC194
            "type": "m.room.message",
cb7b3362e1ceatagen2026-10-25 09:08 UTC195
            "event_id": id,
cb7b3362e1ceatagen2026-10-25 09:08 UTC196
            "sender": sender,
cb7b3362e1ceatagen2026-10-25 09:08 UTC197
            "origin_server_ts": 1,
cb7b3362e1ceatagen2026-10-25 09:08 UTC198
            "content": { "msgtype": "m.text", "body": body },
cb7b3362e1ceatagen2026-10-25 09:08 UTC199
        })
cb7b3362e1ceatagen2026-10-25 09:08 UTC200
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC201
cb7b3362e1ceatagen2026-10-25 09:08 UTC202
    #[test]
cb7b3362e1ceatagen2026-10-25 09:08 UTC203
    fn edits_and_reactions_fold_into_their_targets() {
cb7b3362e1ceatagen2026-10-25 09:08 UTC204
        let mut edit = message("$edit", "@a:x", "* fixed");
cb7b3362e1ceatagen2026-10-25 09:08 UTC205
        edit["content"]["m.new_content"] = json!({ "msgtype": "m.text", "body": "fixed" });
cb7b3362e1ceatagen2026-10-25 09:08 UTC206
        edit["content"]["m.relates_to"] = json!({ "rel_type": "m.replace", "event_id": "$orig" });
cb7b3362e1ceatagen2026-10-25 09:08 UTC207
        let mut foreign_edit = message("$foreign", "@b:x", "* hijack");
cb7b3362e1ceatagen2026-10-25 09:08 UTC208
        foreign_edit["content"]["m.new_content"] = json!({ "msgtype": "m.text", "body": "hijack" });
cb7b3362e1ceatagen2026-10-25 09:08 UTC209
        foreign_edit["content"]["m.relates_to"] =
cb7b3362e1ceatagen2026-10-25 09:08 UTC210
            json!({ "rel_type": "m.replace", "event_id": "$orig" });
cb7b3362e1ceatagen2026-10-25 09:08 UTC211
        let reaction = |id: &str, sender: &str| {
cb7b3362e1ceatagen2026-10-25 09:08 UTC212
            json!({
cb7b3362e1ceatagen2026-10-25 09:08 UTC213
                "type": "m.reaction",
cb7b3362e1ceatagen2026-10-25 09:08 UTC214
                "event_id": id,
cb7b3362e1ceatagen2026-10-25 09:08 UTC215
                "sender": sender,
cb7b3362e1ceatagen2026-10-25 09:08 UTC216
                "origin_server_ts": 1,
cb7b3362e1ceatagen2026-10-25 09:08 UTC217
                "content": { "m.relates_to": {
cb7b3362e1ceatagen2026-10-25 09:08 UTC218
                    "rel_type": "m.annotation", "event_id": "$orig", "key": "👍",
cb7b3362e1ceatagen2026-10-25 09:08 UTC219
                }},
cb7b3362e1ceatagen2026-10-25 09:08 UTC220
            })
cb7b3362e1ceatagen2026-10-25 09:08 UTC221
        };
cb7b3362e1ceatagen2026-10-25 09:08 UTC222
        let mut threaded = message("$threaded", "@b:x", "in a thread");
cb7b3362e1ceatagen2026-10-25 09:08 UTC223
        threaded["content"]["m.relates_to"] =
cb7b3362e1ceatagen2026-10-25 09:08 UTC224
            json!({ "rel_type": "m.thread", "event_id": "$orig" });
cb7b3362e1ceatagen2026-10-25 09:08 UTC225
        let values = [
cb7b3362e1ceatagen2026-10-25 09:08 UTC226
            message("$orig", "@a:x", "typo"),
cb7b3362e1ceatagen2026-10-25 09:08 UTC227
            edit,
cb7b3362e1ceatagen2026-10-25 09:08 UTC228
            foreign_edit,
cb7b3362e1ceatagen2026-10-25 09:08 UTC229
            reaction("$r1", "@b:x"),
cb7b3362e1ceatagen2026-10-25 09:08 UTC230
            reaction("$r2", "@b:x"),
cb7b3362e1ceatagen2026-10-25 09:08 UTC231
            reaction("$r3", "@a:x"),
cb7b3362e1ceatagen2026-10-25 09:08 UTC232
            threaded,
cb7b3362e1ceatagen2026-10-25 09:08 UTC233
            json!({
cb7b3362e1ceatagen2026-10-25 09:08 UTC234
                "type": "m.room.redaction",
cb7b3362e1ceatagen2026-10-25 09:08 UTC235
                "event_id": "$redaction",
cb7b3362e1ceatagen2026-10-25 09:08 UTC236
                "sender": "@a:x",
cb7b3362e1ceatagen2026-10-25 09:08 UTC237
                "origin_server_ts": 1,
cb7b3362e1ceatagen2026-10-25 09:08 UTC238
                "redacts": "$gone",
cb7b3362e1ceatagen2026-10-25 09:08 UTC239
                "content": {},
cb7b3362e1ceatagen2026-10-25 09:08 UTC240
            }),
cb7b3362e1ceatagen2026-10-25 09:08 UTC241
        ];
fbeaaa098107atagen2026-10-25 12:10 UTC242
        let page = normalise_context(
fbeaaa098107atagen2026-10-25 12:10 UTC243
            &values,
fbeaaa098107atagen2026-10-25 12:10 UTC244
            None,
fbeaaa098107atagen2026-10-25 12:10 UTC245
            Some(user_id!("@a:x")),
fbeaaa098107atagen2026-10-25 12:10 UTC246
            room_id!("!room:x"),
fbeaaa098107atagen2026-10-25 12:10 UTC247
            "",
fbeaaa098107atagen2026-10-25 12:10 UTC248
        );
cb7b3362e1ceatagen2026-10-25 09:08 UTC249
        assert_eq!(page.items.len(), 1);
cb7b3362e1ceatagen2026-10-25 09:08 UTC250
        let TimelineItem::Event(event) = &page.items[0].1 else {
cb7b3362e1ceatagen2026-10-25 09:08 UTC251
            panic!("expected an event");
cb7b3362e1ceatagen2026-10-25 09:08 UTC252
        };
cb7b3362e1ceatagen2026-10-25 09:08 UTC253
        let TimelineContent::Message(message) = &event.content else {
cb7b3362e1ceatagen2026-10-25 09:08 UTC254
            panic!("expected a message");
cb7b3362e1ceatagen2026-10-25 09:08 UTC255
        };
cb7b3362e1ceatagen2026-10-25 09:08 UTC256
        assert_eq!(message.body, "fixed");
cb7b3362e1ceatagen2026-10-25 09:08 UTC257
        assert!(message.edited);
cb7b3362e1ceatagen2026-10-25 09:08 UTC258
        assert_eq!(event.reactions.len(), 1);
cb7b3362e1ceatagen2026-10-25 09:08 UTC259
        assert_eq!(event.reactions[0].count, 2);
cb7b3362e1ceatagen2026-10-25 09:08 UTC260
        assert!(event.reactions[0].own);
cb7b3362e1ceatagen2026-10-25 09:08 UTC261
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC262
}