surface message edit history (no edit_message tool - shard commits to its words)

This commit is contained in:
Damocles 2026-05-01 13:45:23 +02:00
parent 9d490f5ca8
commit bc3a4782cc
4 changed files with 75 additions and 5 deletions

View file

@ -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<TimelineItem> = Vec::new();
let mut reactions: Vec<TimelineItem> = 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<OwnedEventId, Vec<(String, i64)>> = HashMap::new();
let mut earliest_message_ts: Option<i64> = 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(),
})
}