cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 1 | use std::collections::HashMap; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 2 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 3 | use ruma::{MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId}; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 4 | use serde_json::Value; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 5 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 6 | use super::{media::MediaRegistration, observed::normalise_observed}; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 7 | use crate::{MediaId, Reaction, ReplyDetails, Sender, TimelineEvent, TimelineItem}; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 8 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 9 | pub(crate) struct ContextPage { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 10 | /// each with the index of the raw event it came from |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 11 | pub(crate) items: Vec<(usize, TimelineItem)>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 12 | pub(crate) media: Vec<MediaRegistration>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 13 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 14 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 15 | struct Annotation<'a> { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 16 | key: &'a str, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 17 | sender: &'a str, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 18 | shortcode: Option<&'a str>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 19 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 20 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 21 | /// raw events in timeline order, shown the way the live room timeline shows them. edits and |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 22 | /// reactions fold into their targets within the page, and redactions, unmatched relations |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 23 | /// and thread replies go. `keep` is never dropped. `media_prefix` keeps one owner's media ids |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 24 | /// apart from another's for the same event |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 25 | pub(crate) fn normalise_context( |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 26 | values: &[Value], |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 27 | keep: Option<usize>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 28 | current_user_id: Option<&UserId>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 29 | room_id: &RoomId, |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 30 | media_prefix: &str, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 31 | ) -> ContextPage { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 32 | let mut edits: HashMap<&str, Vec<&Value>> = HashMap::new(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 33 | let mut annotations: HashMap<&str, Vec<Annotation<'_>>> = HashMap::new(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 34 | let mut shown = Vec::new(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 35 | for (index, value) in values.iter().enumerate() { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 36 | let str_at = |pointer: &str| value.pointer(pointer).and_then(Value::as_str); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 37 | let target = str_at("/content/m.relates_to/event_id"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 38 | if Some(index) == keep { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 39 | shown.push(index); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 40 | continue; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 41 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 42 | match ( |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 43 | str_at("/type"), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 44 | str_at("/content/m.relates_to/rel_type"), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 45 | target, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 46 | ) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 47 | (Some("m.room.redaction"), ..) => {} |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 48 | (Some("m.reaction"), Some("m.annotation"), Some(target)) => { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 49 | if let (Some(key), Some(sender)) = |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 50 | (str_at("/content/m.relates_to/key"), str_at("/sender")) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 51 | { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 52 | annotations.entry(target).or_default().push(Annotation { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 53 | key, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 54 | sender, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 55 | shortcode: str_at("/content/com.beeper.reaction.shortcode") |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 56 | .or_else(|| str_at("/content/shortcode")), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 57 | }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 58 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 59 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 60 | (Some("m.reaction"), ..) => {} |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 61 | // an edit still encrypted keeps its relation in the clear but has no new content |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 62 | (_, Some("m.replace"), Some(target)) => { |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 63 | if value.pointer("/content/m.new_content").is_some() { |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 64 | edits.entry(target).or_default().push(value); |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 65 | } |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 66 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 67 | (_, Some("m.thread"), _) => {} |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 68 | _ => shown.push(index), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 69 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 70 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 71 | let mut media = Vec::new(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 72 | let items = shown |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 73 | .into_iter() |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 74 | .filter_map(|index| { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 75 | let mut value = std::borrow::Cow::Borrowed(&values[index]); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 76 | let event_id = value.get("event_id")?.as_str()?.to_owned(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 77 | let latest_edit = edits.get(event_id.as_str()).and_then(|edits| { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 78 | edits |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 79 | .iter() |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 80 | .rev() |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 81 | .find(|edit| edit.get("sender") == value.get("sender")) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 82 | }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 83 | if let Some(edit) = latest_edit.copied() |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 84 | && value.pointer("/unsigned/m.relations/m.replace").is_none() |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 85 | { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 86 | let unsigned = value |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 87 | .to_mut() |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 88 | .as_object_mut()? |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 89 | .entry("unsigned") |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 90 | .or_insert_with(|| Value::Object(Default::default())); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 91 | let relations = unsigned |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 92 | .as_object_mut()? |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 93 | .entry("m.relations") |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 94 | .or_insert_with(|| Value::Object(Default::default())); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 95 | relations |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 96 | .as_object_mut()? |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 97 | .insert("m.replace".to_owned(), edit.clone()); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 98 | } |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 99 | let identity = format!("{media_prefix}{room_id}\n{event_id}"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 100 | let (mut item, registrations) = normalise_observed(&value, current_user_id, &identity)?; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 101 | media.extend(registrations); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 102 | if let (TimelineItem::Event(event), Some(annotations)) = |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 103 | (&mut item, annotations.get(event_id.as_str())) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 104 | { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 105 | event.reactions = reactions(annotations, current_user_id); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 106 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 107 | Some((index, item)) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 108 | }) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 109 | .collect(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 110 | ContextPage { items, media } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 111 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 112 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 113 | fn reactions(annotations: &[Annotation<'_>], current_user_id: Option<&UserId>) -> Vec<Reaction> { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 114 | let mut reactions: Vec<Reaction> = Vec::new(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 115 | for annotation in annotations { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 116 | let Ok(sender) = OwnedUserId::try_from(annotation.sender) else { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 117 | continue; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 118 | }; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 119 | let index = match reactions.iter().position(|r| r.key == annotation.key) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 120 | Some(index) => index, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 121 | None => { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 122 | reactions.push(Reaction { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 123 | key: annotation.key.to_owned(), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 124 | shortcode: None, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 125 | image: <&MxcUri>::from(annotation.key) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 126 | .is_valid() |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 127 | .then(|| MediaId::plain(annotation.key)), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 128 | count: 0, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 129 | own: false, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 130 | senders: Vec::new(), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 131 | }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 132 | reactions.len() - 1 |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 133 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 134 | }; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 135 | let reaction = &mut reactions[index]; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 136 | if reaction.senders.contains(&sender) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 137 | continue; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 138 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 139 | reaction.own |= current_user_id == Some(sender.as_ref()); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 140 | reaction.senders.push(sender); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 141 | reaction.count = reaction.senders.len(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 142 | if reaction.shortcode.is_none() { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 143 | reaction.shortcode = annotation.shortcode.map(str::to_owned); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 144 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 145 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 146 | reactions |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 147 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 148 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 149 | /// the sender's member data, for each event and every reply quoted in it |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 150 | pub(crate) fn apply_profiles<'a>( |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 151 | events: impl Iterator<Item = &'a mut TimelineEvent>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 152 | profiles: &HashMap<OwnedUserId, Sender>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 153 | ) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 154 | for event in events { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 155 | if let Some(sender) = profiles.get(&event.sender.user_id) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 156 | event.sender = sender.clone(); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 157 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 158 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 159 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 160 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 161 | /// settles every reply, from `targets` or as unavailable |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 162 | pub(crate) fn resolve_replies<'a>( |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 163 | events: impl Iterator<Item = &'a mut TimelineEvent>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 164 | targets: &HashMap<OwnedEventId, TimelineEvent>, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 165 | ) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 166 | for event in events { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 167 | let Some(reply) = event.reply_to.as_mut() else { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 168 | continue; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 169 | }; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 170 | if !matches!(reply.details, ReplyDetails::Pending) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 171 | continue; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 172 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 173 | reply.details = match targets.get(&reply.event_id) { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 174 | Some(target) => ReplyDetails::Ready { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 175 | sender: target.sender.clone(), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 176 | content: Box::new(target.content.clone()), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 177 | timestamp_ms: target.timestamp_ms, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 178 | }, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 179 | None => ReplyDetails::Unavailable, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 180 | }; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 181 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 182 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 183 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 184 | #[cfg(test)] |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 185 | mod tests { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 186 | use ruma::{room_id, user_id}; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 187 | use serde_json::json; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 188 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 189 | use super::*; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 190 | use crate::TimelineContent; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 191 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 192 | fn message(id: &str, sender: &str, body: &str) -> Value { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 193 | json!({ |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 194 | "type": "m.room.message", |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 195 | "event_id": id, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 196 | "sender": sender, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 197 | "origin_server_ts": 1, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 198 | "content": { "msgtype": "m.text", "body": body }, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 199 | }) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 200 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 201 | |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 202 | #[test] |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 203 | fn edits_and_reactions_fold_into_their_targets() { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 204 | let mut edit = message("$edit", "@a:x", "* fixed"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 205 | edit["content"]["m.new_content"] = json!({ "msgtype": "m.text", "body": "fixed" }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 206 | edit["content"]["m.relates_to"] = json!({ "rel_type": "m.replace", "event_id": "$orig" }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 207 | let mut foreign_edit = message("$foreign", "@b:x", "* hijack"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 208 | foreign_edit["content"]["m.new_content"] = json!({ "msgtype": "m.text", "body": "hijack" }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 209 | foreign_edit["content"]["m.relates_to"] = |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 210 | json!({ "rel_type": "m.replace", "event_id": "$orig" }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 211 | let reaction = |id: &str, sender: &str| { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 212 | json!({ |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 213 | "type": "m.reaction", |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 214 | "event_id": id, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 215 | "sender": sender, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 216 | "origin_server_ts": 1, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 217 | "content": { "m.relates_to": { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 218 | "rel_type": "m.annotation", "event_id": "$orig", "key": "👍", |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 219 | }}, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 220 | }) |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 221 | }; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 222 | let mut threaded = message("$threaded", "@b:x", "in a thread"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 223 | threaded["content"]["m.relates_to"] = |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 224 | json!({ "rel_type": "m.thread", "event_id": "$orig" }); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 225 | let values = [ |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 226 | message("$orig", "@a:x", "typo"), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 227 | edit, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 228 | foreign_edit, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 229 | reaction("$r1", "@b:x"), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 230 | reaction("$r2", "@b:x"), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 231 | reaction("$r3", "@a:x"), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 232 | threaded, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 233 | json!({ |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 234 | "type": "m.room.redaction", |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 235 | "event_id": "$redaction", |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 236 | "sender": "@a:x", |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 237 | "origin_server_ts": 1, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 238 | "redacts": "$gone", |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 239 | "content": {}, |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 240 | }), |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 241 | ]; |
1f875b103b27 | atagen | 2026-10-25 10:44 UTC | 242 | let page = normalise_context(&values, None, Some(user_id!("@a:x")), room_id!("!room:x"), ""); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 243 | assert_eq!(page.items.len(), 1); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 244 | let TimelineItem::Event(event) = &page.items[0].1 else { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 245 | panic!("expected an event"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 246 | }; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 247 | let TimelineContent::Message(message) = &event.content else { |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 248 | panic!("expected a message"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 249 | }; |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 250 | assert_eq!(message.body, "fixed"); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 251 | assert!(message.edited); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 252 | assert_eq!(event.reactions.len(), 1); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 253 | assert_eq!(event.reactions[0].count, 2); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 254 | assert!(event.reactions[0].own); |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 255 | } |
cb7b3362e1ce | atagen | 2026-10-25 09:08 UTC | 256 | } |