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()),
|
target_event_id_short: short_eid(target_event_id.as_str()),
|
||||||
key: key.clone(),
|
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 {
|
let earlier_handle = if context_before > 0 && before.len() > context_before as usize {
|
||||||
before.first().and_then(|e| match e {
|
before.first().and_then(|e| match e {
|
||||||
WireEvent::Message { event_id, .. } => Some(event_id.clone()),
|
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,
|
WireEvent::Notice { .. } => None,
|
||||||
})
|
})
|
||||||
} else {
|
} 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;
|
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 messages: Vec<TimelineItem> = Vec::new();
|
||||||
let mut reactions: Vec<TimelineItem> = Vec::new();
|
let mut reactions: Vec<TimelineItem> = Vec::new();
|
||||||
// Edits stashed by target event_id. Walk is newest-first, so edits may
|
let mut stashed_edits: Vec<StashedEdit> = Vec::new();
|
||||||
// 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;
|
let mut earliest_message_ts: Option<i64> = None;
|
||||||
|
|
||||||
for ev in events.iter().rev() {
|
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);
|
let ts = ts_secs_from(orig.origin_server_ts.0);
|
||||||
|
|
||||||
// Edit event? Stash the new body for the target original; do
|
// Edit event? Stash for second pass; don't count toward limit.
|
||||||
// NOT count toward the message limit.
|
|
||||||
if let Some(Relation::Replacement(replacement)) = &orig.content.relates_to {
|
if let Some(Relation::Replacement(replacement)) = &orig.content.relates_to {
|
||||||
if let MessageType::Text(text) = &replacement.new_content.msgtype {
|
if let MessageType::Text(text) = &replacement.new_content.msgtype {
|
||||||
pending_edits
|
stashed_edits.push(StashedEdit {
|
||||||
.entry(replacement.event_id.clone())
|
sender: orig.sender.clone(),
|
||||||
.or_default()
|
target: replacement.event_id.clone(),
|
||||||
.push((text.body.clone(), ts));
|
new_body: text.body.clone(),
|
||||||
|
ts,
|
||||||
|
is_self: &orig.sender == own_user,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -158,46 +166,98 @@ pub async fn load_timeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply pending edits to their target messages. Chain = [original body,
|
// Build per-target chain (original + edits, sorted oldest-first), used
|
||||||
// edits sorted oldest-first]; current `body` becomes the latest, and
|
// for both edit_history population AND old_body lookup on Edit items.
|
||||||
// everything before it ends up in `edit_history`.
|
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 {
|
for item in &mut messages {
|
||||||
if let TimelineItem::Message {
|
if let TimelineItem::Message {
|
||||||
event_id,
|
event_id,
|
||||||
body,
|
body,
|
||||||
ts,
|
|
||||||
edit_history,
|
edit_history,
|
||||||
..
|
..
|
||||||
} = item
|
} = item
|
||||||
{
|
{
|
||||||
let edits = pending_edits.remove(event_id).unwrap_or_default();
|
let Some(chain) = chains.get(event_id) else {
|
||||||
if edits.is_empty() {
|
continue;
|
||||||
|
};
|
||||||
|
if chain.len() <= 1 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut chain: Vec<(String, i64)> = vec![(body.clone(), *ts)];
|
let (latest, _) = chain.last().unwrap();
|
||||||
chain.extend(edits);
|
*body = latest.clone();
|
||||||
chain.sort_by_key(|(_, t)| *t);
|
*edit_history = chain[..chain.len() - 1]
|
||||||
// pop the most recent - that's the visible body
|
.iter()
|
||||||
let (latest, _) = chain.pop().expect("chain has at least the original");
|
|
||||||
*body = latest;
|
|
||||||
*edit_history = chain
|
|
||||||
.into_iter()
|
|
||||||
.map(|(b, t)| EditRecord {
|
.map(|(b, t)| EditRecord {
|
||||||
body: b,
|
body: b.clone(),
|
||||||
ts: t,
|
ts: *t,
|
||||||
ts_human: format!("{} UTC", format_ts(t)),
|
ts_human: format!("{} UTC", format_ts(*t)),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(min_ts) = earliest_message_ts {
|
// Build Edit timeline items (only for edits whose target is in window).
|
||||||
reactions.retain(|r| r.ts() >= min_ts);
|
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(messages);
|
||||||
combined.extend(reactions);
|
combined.extend(reactions);
|
||||||
|
combined.extend(edit_items);
|
||||||
combined.sort_by_key(TimelineItem::ts);
|
combined.sort_by_key(TimelineItem::ts);
|
||||||
Ok(combined)
|
Ok(combined)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
src/types.rs
33
src/types.rs
|
|
@ -43,6 +43,16 @@ pub enum WireEvent {
|
||||||
target_event_id_short: String,
|
target_event_id_short: String,
|
||||||
key: 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
|
/// Synthetic event from the daemon (not a Matrix event). Currently used
|
||||||
/// to tell the shard "you were rate-limited; events held for X seconds."
|
/// to tell the shard "you were rate-limited; events held for X seconds."
|
||||||
Notice {
|
Notice {
|
||||||
|
|
@ -123,31 +133,46 @@ pub enum TimelineItem {
|
||||||
is_self: bool,
|
is_self: bool,
|
||||||
ts: i64,
|
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 {
|
impl TimelineItem {
|
||||||
pub fn ts(&self) -> i64 {
|
pub fn ts(&self) -> i64 {
|
||||||
match self {
|
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> {
|
pub fn event_id(&self) -> Option<&OwnedEventId> {
|
||||||
match self {
|
match self {
|
||||||
Self::Message { event_id, .. } => Some(event_id),
|
Self::Message { event_id, .. } => Some(event_id),
|
||||||
Self::Reaction { .. } => None,
|
Self::Reaction { .. } | Self::Edit { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sender(&self) -> &OwnedUserId {
|
pub fn sender(&self) -> &OwnedUserId {
|
||||||
match self {
|
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 {
|
pub fn is_self(&self) -> bool {
|
||||||
match self {
|
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