diff --git a/src/claude.rs b/src/claude.rs index 06ea397..abad447 100644 --- a/src/claude.rs +++ b/src/claude.rs @@ -116,6 +116,7 @@ pub fn wire_event_from( is_self, ts, in_reply_to, + edit_history, } => { let read_by: Vec = read_markers .get(event_id) @@ -135,6 +136,7 @@ pub fn wire_event_from( body: body.clone(), in_reply_to: in_reply_to.as_ref().map(|e| e.as_str().to_owned()), read_by, + edit_history: edit_history.clone(), } } TimelineItem::Reaction { diff --git a/src/socket.rs b/src/socket.rs index 32efaae..4f23183 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -379,6 +379,7 @@ async fn fetch_event( body: text.body.clone(), in_reply_to, read_by: Vec::new(), + edit_history: Vec::new(), }) } matrix_sdk::ruma::events::AnySyncMessageLikeEvent::Reaction( diff --git a/src/timeline.rs b/src/timeline.rs index 6f05297..0acc70a 100644 --- a/src/timeline.rs +++ b/src/timeline.rs @@ -5,7 +5,7 @@ use matrix_sdk::{ ruma::{OwnedEventId, OwnedUserId, events::room::message::MessageType}, }; -use crate::types::TimelineItem; +use crate::types::{EditRecord, TimelineItem}; /// Format a unix-seconds timestamp as `YYYY-MM-DD HH:MM` UTC. Returns "?" for 0. pub fn format_ts(secs: i64) -> String { @@ -83,8 +83,13 @@ pub async fn load_timeline( let (cache, _handles) = room.event_cache().await?; let events = cache.events().await; + use matrix_sdk::ruma::events::room::message::Relation; + let mut messages: Vec = Vec::new(); let mut reactions: Vec = Vec::new(); + // Edits stashed by target event_id. Walk is newest-first, so edits may + // arrive before their target original. (body, ts) pairs. + let mut pending_edits: HashMap> = HashMap::new(); let mut earliest_message_ts: Option = None; for ev in events.iter().rev() { @@ -100,17 +105,28 @@ pub async fn load_timeline( matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage( matrix_sdk::ruma::events::SyncMessageLikeEvent::Original(orig), ) => { + let ts = ts_secs_from(orig.origin_server_ts.0); + + // Edit event? Stash the new body for the target original; do + // NOT count toward the message limit. + if let Some(Relation::Replacement(replacement)) = &orig.content.relates_to { + if let MessageType::Text(text) = &replacement.new_content.msgtype { + pending_edits + .entry(replacement.event_id.clone()) + .or_default() + .push((text.body.clone(), ts)); + } + continue; + } + if messages.len() >= limit { continue; } let MessageType::Text(text) = &orig.content.msgtype else { continue; }; - let ts = ts_secs_from(orig.origin_server_ts.0); let in_reply_to = match &orig.content.relates_to { - Some(matrix_sdk::ruma::events::room::message::Relation::Reply { - in_reply_to, - }) => Some(in_reply_to.event_id.clone()), + Some(Relation::Reply { in_reply_to }) => Some(in_reply_to.event_id.clone()), _ => None, }; if earliest_message_ts.is_none_or(|e| ts < e) { @@ -123,6 +139,7 @@ pub async fn load_timeline( is_self: &orig.sender == own_user, ts, in_reply_to, + edit_history: Vec::new(), }); } matrix_sdk::ruma::events::AnySyncMessageLikeEvent::Reaction( @@ -141,6 +158,39 @@ pub async fn load_timeline( } } + // Apply pending edits to their target messages. Chain = [original body, + // edits sorted oldest-first]; current `body` becomes the latest, and + // everything before it ends up in `edit_history`. + for item in &mut messages { + if let TimelineItem::Message { + event_id, + body, + ts, + edit_history, + .. + } = item + { + let edits = pending_edits.remove(event_id).unwrap_or_default(); + if edits.is_empty() { + continue; + } + let mut chain: Vec<(String, i64)> = vec![(body.clone(), *ts)]; + chain.extend(edits); + chain.sort_by_key(|(_, t)| *t); + // pop the most recent - that's the visible body + let (latest, _) = chain.pop().expect("chain has at least the original"); + *body = latest; + *edit_history = chain + .into_iter() + .map(|(b, t)| EditRecord { + body: b, + ts: t, + ts_human: format!("{} UTC", format_ts(t)), + }) + .collect(); + } + } + if let Some(min_ts) = earliest_message_ts { reactions.retain(|r| r.ts() >= min_ts); } @@ -182,6 +232,7 @@ pub async fn fetch_message( is_self: &orig.sender == own_user, ts, in_reply_to: None, + edit_history: Vec::new(), }) } diff --git a/src/types.rs b/src/types.rs index af75430..b9625e8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,14 @@ use matrix_sdk::{ }; use serde::{Deserialize, Serialize}; +/// One prior version of an edited message. +#[derive(Debug, Clone, Serialize)] +pub struct EditRecord { + pub body: String, + pub ts: i64, + pub ts_human: String, +} + /// Serializable shape for one timeline event, used both in matrix_turn JSON /// (input to the shard) and tool response JSON (get_room_history, /// fetch_event). @@ -22,6 +30,9 @@ pub enum WireEvent { body: String, in_reply_to: Option, read_by: Vec, + /// Prior versions of this message, oldest first. Empty when the + /// message has never been edited. + edit_history: Vec, }, Reaction { sender: String, @@ -94,11 +105,16 @@ pub enum TimelineItem { Message { event_id: OwnedEventId, sender: OwnedUserId, + /// Latest version of the message body. If the user edited this message, + /// this is the most recent edit's content. body: String, is_self: bool, /// Unix seconds. 0 if unknown. ts: i64, in_reply_to: Option, + /// Prior versions of this message (oldest first), excluding the + /// current `body`. Empty when the message has never been edited. + edit_history: Vec, }, Reaction { sender: OwnedUserId,