edits as standalone timeline events with old_body/new_body

This commit is contained in:
Damocles 2026-05-01 13:50:52 +02:00
parent bc3a4782cc
commit 9171290ae7
4 changed files with 137 additions and 34 deletions

View file

@ -85,11 +85,17 @@ pub async fn load_timeline(
use matrix_sdk::ruma::events::room::message::Relation;
struct StashedEdit {
sender: OwnedUserId,
target: OwnedEventId,
new_body: String,
ts: i64,
is_self: bool,
}
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 stashed_edits: Vec<StashedEdit> = Vec::new();
let mut earliest_message_ts: Option<i64> = None;
for ev in events.iter().rev() {
@ -107,14 +113,16 @@ pub async fn load_timeline(
) => {
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.
// Edit event? Stash for second pass; don't count toward 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));
stashed_edits.push(StashedEdit {
sender: orig.sender.clone(),
target: replacement.event_id.clone(),
new_body: text.body.clone(),
ts,
is_self: &orig.sender == own_user,
});
}
continue;
}
@ -158,46 +166,98 @@ 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`.
// Build per-target chain (original + edits, sorted oldest-first), used
// for both edit_history population AND old_body lookup on Edit items.
let mut chains: HashMap<OwnedEventId, Vec<(String, i64)>> = HashMap::new();
for msg in &messages {
if let TimelineItem::Message { event_id, body, ts, .. } = msg {
chains
.entry(event_id.clone())
.or_default()
.push((body.clone(), *ts));
}
}
for edit in &stashed_edits {
chains
.entry(edit.target.clone())
.or_default()
.push((edit.new_body.clone(), edit.ts));
}
for chain in chains.values_mut() {
chain.sort_by_key(|(_, t)| *t);
}
// Update messages with edit_history + latest body.
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() {
let Some(chain) = chains.get(event_id) else {
continue;
};
if chain.len() <= 1 {
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()
let (latest, _) = chain.last().unwrap();
*body = latest.clone();
*edit_history = chain[..chain.len() - 1]
.iter()
.map(|(b, t)| EditRecord {
body: b,
ts: t,
ts_human: format!("{} UTC", format_ts(t)),
body: b.clone(),
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);
// Build Edit timeline items (only for edits whose target is in window).
let in_window_targets: std::collections::HashSet<OwnedEventId> = messages
.iter()
.filter_map(|m| match m {
TimelineItem::Message { event_id, .. } => Some(event_id.clone()),
_ => None,
})
.collect();
let mut edit_items: Vec<TimelineItem> = Vec::new();
for edit in &stashed_edits {
if !in_window_targets.contains(&edit.target) {
continue;
}
let chain = chains.get(&edit.target).expect("chain exists for in-window target");
let pos = chain
.iter()
.position(|(b, t)| *t == edit.ts && *b == edit.new_body)
.unwrap_or(0);
let old_body = if pos > 0 {
chain[pos - 1].0.clone()
} else {
String::new()
};
edit_items.push(TimelineItem::Edit {
sender: edit.sender.clone(),
target_event_id: edit.target.clone(),
old_body,
new_body: edit.new_body.clone(),
is_self: edit.is_self,
ts: edit.ts,
});
}
let mut combined: Vec<TimelineItem> = Vec::with_capacity(messages.len() + reactions.len());
if let Some(min_ts) = earliest_message_ts {
reactions.retain(|r| r.ts() >= min_ts);
edit_items.retain(|e| e.ts() >= min_ts);
}
let mut combined: Vec<TimelineItem> =
Vec::with_capacity(messages.len() + reactions.len() + edit_items.len());
combined.extend(messages);
combined.extend(reactions);
combined.extend(edit_items);
combined.sort_by_key(TimelineItem::ts);
Ok(combined)
}