Skip to content

Commit 8de23ed

Browse files
committed
feat(edit): Add edit revisions list
Signed-off-by: bxdxnn <267911624+bxdxnn@users.noreply.github.com>
1 parent c0fde33 commit 8de23ed

11 files changed

Lines changed: 132 additions & 31 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a new `revisions` field for `EventTimelineItem` that stores the edit revisions of the item.

bindings/matrix-sdk-ffi/src/timeline/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,7 @@ pub struct EventTimelineItem {
10221022
read_receipts: HashMap<String, Receipt>,
10231023
origin: Option<EventItemOrigin>,
10241024
can_be_replied_to: bool,
1025+
revisions: Vec<EditRevisionRecord>,
10251026
lazy_provider: Arc<LazyTimelineItemProvider>,
10261027
}
10271028

@@ -1048,6 +1049,10 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
10481049
read_receipts,
10491050
origin: item.origin(),
10501051
can_be_replied_to: item.can_be_replied_to(),
1052+
revisions: item.edit_revisions().iter().map(|r| EditRevisionRecord {
1053+
body: r.body.clone(),
1054+
timestamp: r.timestamp.0.into(),
1055+
}).collect(),
10511056
lazy_provider,
10521057
}
10531058
}
@@ -1064,6 +1069,12 @@ impl From<ruma::events::receipt::Receipt> for Receipt {
10641069
}
10651070
}
10661071

1072+
#[derive(Clone, uniffi::Record)]
1073+
pub struct EditRevisionRecord {
1074+
body: String,
1075+
timestamp: u64,
1076+
}
1077+
10671078
#[derive(Clone, uniffi::Record)]
10681079
pub struct EventTimelineItemDebugInfo {
10691080
model: String,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a new `revisions` field for `EventTimelineItem` that stores the edit revisions of the item.

crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ use tracing::{error, info, trace, warn};
5454

5555
use super::{ObservableItemsTransaction, rfind_event_by_item_id};
5656
use crate::timeline::{
57-
BeaconInfo, EventTimelineItem, LiveLocationState, MsgLikeContent, MsgLikeKind, PollState,
58-
ReactionInfo, ReactionStatus, TimelineEventItemId, TimelineItem, TimelineItemContent,
57+
BeaconInfo, EditRevision, EventTimelineItem, LiveLocationState, MsgLikeContent, MsgLikeKind,
58+
PollState, ReactionInfo, ReactionStatus, TimelineEventItemId, TimelineItem, TimelineItemContent,
5959
event_item::beacon_info_matches,
6060
};
6161

@@ -943,9 +943,31 @@ fn edit_item(
943943
let mut new_msg = msg.clone();
944944
new_msg.apply_edit(replacement.new_content);
945945

946+
let edit_revision = if is_local_echo {
947+
// For local echoes, we don't have the edit JSON yet,
948+
// but record the edit to avoid capturing the already-edited
949+
// content as the original when the server echo arrives.
950+
Some(EditRevision {
951+
body: new_msg.body().to_owned(),
952+
timestamp: item.timestamp(),
953+
edit_json: None,
954+
})
955+
} else {
956+
edit_json.as_ref().map(|json| {
957+
let ts: Option<MilliSecondsSinceUnixEpoch> =
958+
json.get_field("origin_server_ts").ok().flatten()
959+
.map(|v: ruma::UInt| MilliSecondsSinceUnixEpoch(v.into()));
960+
EditRevision {
961+
body: new_msg.body().to_owned(),
962+
timestamp: ts.unwrap_or(item.timestamp()),
963+
edit_json: edit_json.clone(),
964+
}
965+
})
966+
};
967+
946968
let new_item = item.with_content_and_latest_edit(
947969
TimelineItemContent::MsgLike(content.with_kind(MsgLikeKind::Message(new_msg))),
948-
edit_json,
970+
edit_revision,
949971
);
950972
*item = Cow::Owned(new_item);
951973
}
@@ -960,7 +982,7 @@ fn edit_item(
960982
TimelineItemContent::MsgLike(
961983
content.with_kind(MsgLikeKind::Poll(new_poll_state)),
962984
),
963-
edit_json,
985+
None,
964986
);
965987
*item = Cow::Owned(new_item);
966988
} else {

crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ mod tests {
255255
is_highlighted: false,
256256
encryption_info: None,
257257
original_json: None,
258-
latest_edit_json: None,
258+
edit_revisions: vec![],
259259
origin: RemoteEventOrigin::Sync,
260260
});
261261

@@ -308,7 +308,7 @@ mod tests {
308308
verification_state: VerificationState::Verified,
309309
})),
310310
original_json: None,
311-
latest_edit_json: None,
311+
edit_revisions: vec![],
312312
origin: RemoteEventOrigin::Sync,
313313
});
314314

crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ mod observable_items_tests {
756756
is_highlighted: false,
757757
encryption_info: None,
758758
original_json: None,
759-
latest_edit_json: None,
759+
edit_revisions: vec![],
760760
origin: RemoteEventOrigin::Sync,
761761
}),
762762
false,

crates/matrix-sdk-ui/src/timeline/date_dividers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ mod tests {
677677
is_highlighted: false,
678678
encryption_info: None,
679679
original_json: None,
680-
latest_edit_json: None,
680+
edit_revisions: vec![],
681681
origin: crate::timeline::event_item::RemoteEventOrigin::Sync,
682682
});
683683
EventTimelineItem::new(

crates/matrix-sdk-ui/src/timeline/event_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -994,7 +994,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
994994
is_highlighted: self.ctx.is_highlighted,
995995
encryption_info: encryption_info.clone(),
996996
original_json: Some(raw_event.clone()),
997-
latest_edit_json: None,
997+
edit_revisions: vec![],
998998
origin,
999999
}
10001000
.into()

crates/matrix-sdk-ui/src/timeline/event_item/mod.rs

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,23 @@ pub(crate) enum TimelineItemHandle<'a> {
124124
Local(&'a SendHandle),
125125
}
126126

127+
/// A single revision in the edit history of a message.
128+
#[derive(Clone, Debug)]
129+
pub struct EditRevision {
130+
/// The body text of the message after this revision.
131+
pub body: String,
132+
/// The timestamp of the event that created this revision.
133+
pub timestamp: MilliSecondsSinceUnixEpoch,
134+
/// The raw JSON of the edit event, if available.
135+
/// This is `None` for the original revision and for local echoes
136+
/// that haven't been echoed by the server yet.
137+
///
138+
/// Only the latest revision retains its `edit_json`; all earlier
139+
/// revisions have this field cleared to `None` to avoid storing
140+
/// copies of raw event JSON for every edit.
141+
pub edit_json: Option<Raw<AnySyncTimelineEvent>>,
142+
}
143+
127144
/// A container for temporarily holding onto data that is going to be erased by
128145
/// a redaction once the server plays it back.
129146
#[derive(Clone, Debug)]
@@ -134,8 +151,8 @@ pub(super) struct UnredactedEventTimelineItem {
134151
/// JSON of the original event.
135152
pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
136153

137-
/// JSON of the latest edit to this item.
138-
pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
154+
/// All edits to this item, in chronological order.
155+
pub(crate) edit_revisions: Vec<EditRevision>,
139156
}
140157

141158
impl EventTimelineItem {
@@ -453,10 +470,20 @@ impl EventTimelineItem {
453470
}
454471

455472
/// Get the raw JSON representation of the latest edit, if any.
473+
/// Returns the raw JSON of the most recent edit revision, if available.
474+
/// Use `edit_revisions()` for structured access to the full edit history.
456475
pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
476+
self.edit_revisions().last()?.edit_json.as_ref()
477+
}
478+
479+
/// Get all revisions of this item, in chronological order.
480+
///
481+
/// The first entry is the original revision, followed by each edit
482+
/// in the order they were applied.
483+
pub fn edit_revisions(&self) -> &[EditRevision] {
457484
match &self.kind {
458-
EventTimelineItemKind::Local(_) => None,
459-
EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
485+
EventTimelineItemKind::Local(_) => &[],
486+
EventTimelineItemKind::Remote(remote_event) => &remote_event.edit_revisions,
460487
}
461488
}
462489

@@ -497,20 +524,42 @@ impl EventTimelineItem {
497524
new
498525
}
499526

500-
/// Clone the current event item, and update its content.
527+
/// Clone the current event item, and update its content with an edit.
501528
///
502-
/// Optionally update `latest_edit_json` if the update is an edit received
503-
/// from the server.
529+
/// If this is the first edit, the original revision is captured
530+
/// automatically from the item's current state. The `edit_revision`
531+
/// contains the edit's data (timestamp and the new body).
532+
/// If `edit_revision` is `None`, no edit revision is recorded
533+
/// (e.g. for non-message edits like polls).
504534
pub(super) fn with_content_and_latest_edit(
505535
&self,
506536
new_content: TimelineItemContent,
507-
edit_json: Option<Raw<AnySyncTimelineEvent>>,
537+
edit_revision: Option<EditRevision>,
508538
) -> Self {
509539
let mut new = self.clone();
510-
new.content = new_content;
511540
if let EventTimelineItemKind::Remote(r) = &mut new.kind {
512-
r.latest_edit_json = edit_json;
541+
// If this is the first edit, capture the original as revision 0
542+
if edit_revision.is_some() && r.edit_revisions.is_empty() {
543+
if let Some(body) = extract_body_from_content(&new.content) {
544+
r.edit_revisions.push(EditRevision {
545+
body,
546+
timestamp: new.timestamp,
547+
edit_json: None,
548+
});
549+
}
550+
}
551+
if let Some(revision) = edit_revision {
552+
r.edit_revisions.push(revision);
553+
// Keep `edit_json` only on the latest revision to avoid
554+
// storing copies of raw edit event JSON for every revision.
555+
if r.edit_revisions.len() > 1 {
556+
for rev in r.edit_revisions.iter_mut().rev().skip(1) {
557+
rev.edit_json = None;
558+
}
559+
}
560+
}
513561
}
562+
new.content = new_content;
514563
new
515564
}
516565

@@ -534,10 +583,16 @@ impl EventTimelineItem {
534583

535584
/// Create a clone of the current item, with content that's been redacted.
536585
pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
537-
let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
538-
content: self.content.clone(),
539-
original_json: self.original_json().cloned(),
540-
latest_edit_json: self.latest_edit_json().cloned(),
586+
let unredacted_item = is_local.then(|| {
587+
let edit_revisions = match &self.kind {
588+
EventTimelineItemKind::Remote(r) => r.edit_revisions.clone(),
589+
EventTimelineItemKind::Local(_) => vec![],
590+
};
591+
UnredactedEventTimelineItem {
592+
content: self.content.clone(),
593+
original_json: self.original_json().cloned(),
594+
edit_revisions,
595+
}
541596
});
542597
let content = self.content.redact(rules);
543598
let kind = match &self.kind {
@@ -567,7 +622,7 @@ impl EventTimelineItem {
567622
EventTimelineItemKind::Remote(r) => {
568623
EventTimelineItemKind::Remote(RemoteEventTimelineItem {
569624
original_json: unredacted_item.original_json.clone(),
570-
latest_edit_json: unredacted_item.latest_edit_json.clone(),
625+
edit_revisions: unredacted_item.edit_revisions.clone(),
571626
..r.clone()
572627
})
573628
}
@@ -914,6 +969,17 @@ impl From<ShieldStateCode> for TimelineEventShieldStateCode {
914969
}
915970
}
916971

972+
/// Extract the body text from a `TimelineItemContent`, if it's a message.
973+
pub(crate) fn extract_body_from_content(content: &TimelineItemContent) -> Option<String> {
974+
match content {
975+
TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
976+
MsgLikeKind::Message(message) => Some(message.body().to_owned()),
977+
_ => None,
978+
},
979+
_ => None,
980+
}
981+
}
982+
917983
#[cfg(test)]
918984
mod tests {
919985
use std::time::Duration;
@@ -985,7 +1051,7 @@ mod tests {
9851051
is_highlighted: false,
9861052
encryption_info: None,
9871053
original_json,
988-
latest_edit_json: None,
1054+
edit_revisions: vec![],
9891055
origin: RemoteEventOrigin::Sync,
9901056
}),
9911057
false,

crates/matrix-sdk-ui/src/timeline/event_item/remote.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub(in crate::timeline) struct RemoteEventTimelineItem {
5050

5151
/// JSON of the original event.
5252
///
53-
/// If the event is edited, this *won't* change, instead `latest_edit_json`
53+
/// If the event is edited, this *won't* change, instead `edit_revisions`
5454
/// will be updated.
5555
///
5656
/// This field always starts out as `Some(_)`, but is set to `None` when the
@@ -60,8 +60,8 @@ pub(in crate::timeline) struct RemoteEventTimelineItem {
6060
/// a clear need for that.
6161
pub original_json: Option<Raw<AnySyncTimelineEvent>>,
6262

63-
/// JSON of the latest edit to this item.
64-
pub latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
63+
/// All edits to this item, in chronological order.
64+
pub edit_revisions: Vec<super::EditRevision>,
6565

6666
/// Where we got this event from: A sync response or pagination.
6767
pub origin: RemoteEventOrigin,
@@ -70,7 +70,7 @@ pub(in crate::timeline) struct RemoteEventTimelineItem {
7070
impl RemoteEventTimelineItem {
7171
/// Clone the current event item, and redacts its fields.
7272
pub fn redact(&self) -> Self {
73-
Self { original_json: None, latest_edit_json: None, ..self.clone() }
73+
Self { original_json: None, edit_revisions: vec![], ..self.clone() }
7474
}
7575
}
7676

@@ -98,7 +98,7 @@ impl fmt::Debug for RemoteEventTimelineItem {
9898
is_own,
9999
encryption_info,
100100
original_json: _,
101-
latest_edit_json: _,
101+
edit_revisions: _,
102102
is_highlighted,
103103
origin,
104104
} = self;

0 commit comments

Comments
 (0)