edits as standalone timeline events with old_body/new_body
This commit is contained in:
parent
bc3a4782cc
commit
9171290ae7
4 changed files with 137 additions and 34 deletions
|
|
@ -154,6 +154,23 @@ pub fn wire_event_from(
|
|||
target_event_id_short: short_eid(target_event_id.as_str()),
|
||||
key: key.clone(),
|
||||
},
|
||||
TimelineItem::Edit {
|
||||
sender,
|
||||
target_event_id,
|
||||
old_body,
|
||||
new_body,
|
||||
is_self,
|
||||
ts,
|
||||
} => WireEvent::Edit {
|
||||
sender: sender.as_str().to_owned(),
|
||||
is_self: *is_self,
|
||||
ts: *ts,
|
||||
ts_human: format!("{} UTC", format_ts(*ts)),
|
||||
target_event_id: target_event_id.as_str().to_owned(),
|
||||
target_event_id_short: short_eid(target_event_id.as_str()),
|
||||
old_body: old_body.clone(),
|
||||
new_body: new_body.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -410,7 +410,8 @@ async fn fetch_event(
|
|||
let earlier_handle = if context_before > 0 && before.len() > context_before as usize {
|
||||
before.first().and_then(|e| match e {
|
||||
WireEvent::Message { event_id, .. } => Some(event_id.clone()),
|
||||
WireEvent::Reaction { target_event_id, .. } => Some(target_event_id.clone()),
|
||||
WireEvent::Reaction { target_event_id, .. }
|
||||
| WireEvent::Edit { target_event_id, .. } => Some(target_event_id.clone()),
|
||||
WireEvent::Notice { .. } => None,
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
118
src/timeline.rs
118
src/timeline.rs
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
33
src/types.rs
33
src/types.rs
|
|
@ -43,6 +43,16 @@ pub enum WireEvent {
|
|||
target_event_id_short: String,
|
||||
key: String,
|
||||
},
|
||||
Edit {
|
||||
sender: String,
|
||||
is_self: bool,
|
||||
ts: i64,
|
||||
ts_human: String,
|
||||
target_event_id: String,
|
||||
target_event_id_short: String,
|
||||
old_body: String,
|
||||
new_body: String,
|
||||
},
|
||||
/// Synthetic event from the daemon (not a Matrix event). Currently used
|
||||
/// to tell the shard "you were rate-limited; events held for X seconds."
|
||||
Notice {
|
||||
|
|
@ -123,31 +133,46 @@ pub enum TimelineItem {
|
|||
is_self: bool,
|
||||
ts: i64,
|
||||
},
|
||||
/// A single edit applied to a message. Surfaced as its own chronological
|
||||
/// event so the shard sees edits as they happen. The target message also
|
||||
/// has its `edit_history` updated for at-a-glance reference.
|
||||
Edit {
|
||||
sender: OwnedUserId,
|
||||
target_event_id: OwnedEventId,
|
||||
old_body: String,
|
||||
new_body: String,
|
||||
is_self: bool,
|
||||
ts: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl TimelineItem {
|
||||
pub fn ts(&self) -> i64 {
|
||||
match self {
|
||||
Self::Message { ts, .. } | Self::Reaction { ts, .. } => *ts,
|
||||
Self::Message { ts, .. } | Self::Reaction { ts, .. } | Self::Edit { ts, .. } => *ts,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event_id(&self) -> Option<&OwnedEventId> {
|
||||
match self {
|
||||
Self::Message { event_id, .. } => Some(event_id),
|
||||
Self::Reaction { .. } => None,
|
||||
Self::Reaction { .. } | Self::Edit { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> &OwnedUserId {
|
||||
match self {
|
||||
Self::Message { sender, .. } | Self::Reaction { sender, .. } => sender,
|
||||
Self::Message { sender, .. }
|
||||
| Self::Reaction { sender, .. }
|
||||
| Self::Edit { sender, .. } => sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_self(&self) -> bool {
|
||||
match self {
|
||||
Self::Message { is_self, .. } | Self::Reaction { is_self, .. } => *is_self,
|
||||
Self::Message { is_self, .. }
|
||||
| Self::Reaction { is_self, .. }
|
||||
| Self::Edit { is_self, .. } => *is_self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue