Skip to content

Commit ba8f1bb

Browse files
committed
feat(timeline): handle local echoes of redactions
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
1 parent c288212 commit ba8f1bb

File tree

10 files changed

+156
-16
lines changed

10 files changed

+156
-16
lines changed

crates/matrix-sdk-ui/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
88

99
### Bug Fixes
1010

11+
- Handle local echoes of redactions in the timeline.
12+
([#6250](https://github.com/matrix-org/matrix-rust-sdk/pull/6250))
1113
- Include secondary relations when re-initializing a threaded timeline after a lag.
1214
([#6209](https://github.com/matrix-org/matrix-rust-sdk/pull/6209))
1315
- Ensure that the display name of a `Room` in a `NotificationStatus` coming

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,12 @@ pub(crate) enum AggregationKind {
126126
},
127127

128128
/// An event has been redacted.
129-
Redaction,
129+
Redaction {
130+
/// Whether this aggregation results from the local echo of a redaction.
131+
/// Local echoes of redactions are applied reversibly whereas remote
132+
/// echoes of redactions are applied irreversibly.
133+
is_local: bool,
134+
},
130135

131136
/// An event has been edited.
132137
///
@@ -237,11 +242,15 @@ impl Aggregation {
237242
}
238243
}
239244

240-
AggregationKind::Redaction => {
241-
if event.content().is_redacted() {
245+
AggregationKind::Redaction { is_local } => {
246+
let is_local_redacted =
247+
event.content().is_redacted() && event.unredacted_content.is_some();
248+
let is_remote_redacted =
249+
event.content().is_redacted() && event.unredacted_content.is_none();
250+
if *is_local && is_local_redacted || !*is_local && is_remote_redacted {
242251
ApplyAggregationResult::LeftItemIntact
243252
} else {
244-
let new_item = event.redact(&rules.redaction);
253+
let new_item = event.redact(&rules.redaction, *is_local);
245254
*event = Cow::Owned(new_item);
246255
ApplyAggregationResult::UpdatedItem
247256
}
@@ -352,9 +361,21 @@ impl Aggregation {
352361
ApplyAggregationResult::Error(AggregationError::CantUndoPollEnd)
353362
}
354363

355-
AggregationKind::Redaction => {
356-
// Redactions are not reversible.
357-
ApplyAggregationResult::Error(AggregationError::CantUndoRedaction)
364+
AggregationKind::Redaction { is_local } => {
365+
let is_local_redacted = event.unredacted_content.is_some();
366+
if *is_local {
367+
if is_local_redacted {
368+
// Unapply local redaction.
369+
*event = Cow::Owned(event.unredact());
370+
ApplyAggregationResult::UpdatedItem
371+
} else {
372+
// Event isn't locally redacted. Nothing to do.
373+
ApplyAggregationResult::LeftItemIntact
374+
}
375+
} else {
376+
// Remote redactions are not reversible.
377+
ApplyAggregationResult::Error(AggregationError::CantUndoRedaction)
378+
}
358379
}
359380

360381
AggregationKind::Reaction { key, sender, .. } => {
@@ -477,7 +498,7 @@ impl Aggregations {
477498
pub fn add(&mut self, related_to: TimelineEventItemId, aggregation: Aggregation) {
478499
// If the aggregation is a redaction, it invalidates all the other aggregations;
479500
// remove them.
480-
if matches!(aggregation.kind, AggregationKind::Redaction) {
501+
if matches!(aggregation.kind, AggregationKind::Redaction { .. }) {
481502
for agg in self.related_events.remove(&related_to).unwrap_or_default() {
482503
self.inverted_map.remove(&agg.own_id);
483504
}
@@ -488,7 +509,7 @@ impl Aggregations {
488509
if let Some(previous_aggregations) = self.related_events.get(&related_to)
489510
&& previous_aggregations
490511
.iter()
491-
.any(|agg| matches!(agg.kind, AggregationKind::Redaction))
512+
.any(|agg| matches!(agg.kind, AggregationKind::Redaction { .. }))
492513
{
493514
return;
494515
}
@@ -698,7 +719,7 @@ impl Aggregations {
698719
AggregationKind::PollResponse { .. }
699720
| AggregationKind::PollEnd { .. }
700721
| AggregationKind::Edit(..)
701-
| AggregationKind::Redaction
722+
| AggregationKind::Redaction { .. }
702723
| AggregationKind::BeaconUpdate { .. }
703724
| AggregationKind::BeaconStop { .. } => {
704725
// Nothing particular to do.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ mod tests {
239239
None,
240240
timestamp(),
241241
TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
242+
None,
242243
event_kind,
243244
true,
244245
)),
@@ -282,6 +283,7 @@ mod tests {
282283
UtdCause::Unknown,
283284
),
284285
)),
286+
None,
285287
event_kind,
286288
true,
287289
)),
@@ -329,6 +331,7 @@ mod tests {
329331
None,
330332
None,
331333
),
334+
None,
332335
event_kind,
333336
true,
334337
)),

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,8 +1269,21 @@ impl<P: RoomDataProvider> TimelineController<P> {
12691269
self.handle_local_reaction(key, send_handle, applies_to).await;
12701270
}
12711271

1272-
LocalEchoContent::Redaction { .. } => {
1273-
// TODO: Handle local redactions in the timeline.
1272+
LocalEchoContent::Redaction { redacts, send_error, .. } => {
1273+
self.handle_local_redaction(echo.transaction_id.clone(), redacts).await;
1274+
1275+
if let Some(send_error) = send_error {
1276+
self.update_event_send_state(
1277+
&echo.transaction_id,
1278+
EventSendState::SendingFailed {
1279+
error: Arc::new(matrix_sdk::Error::SendQueueWedgeError(Box::new(
1280+
send_error,
1281+
))),
1282+
is_recoverable: false,
1283+
},
1284+
)
1285+
.await;
1286+
}
12741287
}
12751288
}
12761289
}
@@ -1312,6 +1325,34 @@ impl<P: RoomDataProvider> TimelineController<P> {
13121325
tr.commit();
13131326
}
13141327

1328+
/// Applies a local echo of a redaction.
1329+
pub(super) async fn handle_local_redaction(
1330+
&self,
1331+
txn_id: OwnedTransactionId,
1332+
redacts: OwnedEventId,
1333+
) {
1334+
let mut state = self.state.write().await;
1335+
let mut tr = state.transaction();
1336+
1337+
let target = TimelineEventItemId::EventId(redacts);
1338+
1339+
let aggregation = Aggregation::new(
1340+
TimelineEventItemId::TransactionId(txn_id),
1341+
AggregationKind::Redaction { is_local: true },
1342+
);
1343+
1344+
tr.meta.aggregations.add(target.clone(), aggregation.clone());
1345+
find_item_and_apply_aggregation(
1346+
&tr.meta.aggregations,
1347+
&mut tr.items,
1348+
&target,
1349+
aggregation,
1350+
&tr.meta.room_version_rules,
1351+
);
1352+
1353+
tr.commit();
1354+
}
1355+
13151356
/// Handle a single room send queue update.
13161357
pub(crate) async fn handle_room_send_queue_update(&self, update: RoomSendQueueUpdate) {
13171358
match update {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,7 @@ mod observable_items_tests {
748748
in_reply_to: None,
749749
thread_summary: None,
750750
}),
751+
None,
751752
EventTimelineItemKind::Remote(RemoteEventTimelineItem {
752753
event_id: event_id.parse().unwrap(),
753754
transaction_id: None,
@@ -784,6 +785,7 @@ mod observable_items_tests {
784785
in_reply_to: None,
785786
thread_summary: None,
786787
}),
788+
None,
787789
EventTimelineItemKind::Local(LocalEventTimelineItem {
788790
send_state: EventSendState::NotSentYet { progress: None },
789791
transaction_id: transaction_id.into(),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,7 @@ mod tests {
687687
None,
688688
timestamp,
689689
TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
690+
None,
690691
event_kind,
691692
false,
692693
)

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,8 +830,12 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
830830
}
831831

832832
let target = TimelineEventItemId::EventId(redacted.clone());
833-
let aggregation =
834-
Aggregation::new(self.ctx.flow.timeline_item_id(), AggregationKind::Redaction);
833+
let aggregation = Aggregation::new(
834+
self.ctx.flow.timeline_item_id(),
835+
AggregationKind::Redaction {
836+
is_local: false, // We can only get here for remote echoes of redactions.
837+
},
838+
);
835839
self.meta.aggregations.add(target.clone(), aggregation.clone());
836840

837841
find_item_and_apply_aggregation(
@@ -951,6 +955,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
951955
forwarder_profile,
952956
timestamp,
953957
content,
958+
None,
954959
kind,
955960
is_room_encrypted,
956961
);

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,13 @@ pub struct EventTimelineItem {
7878
pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
7979
/// The timestamp of the event.
8080
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
81-
/// The content of the event.
81+
/// The content of the event. Might be redacted if a redaction for this
82+
/// event is currently being sent or has been received from the server.
8283
pub(super) content: TimelineItemContent,
84+
/// If a redaction for this event is currently being sent but the server
85+
/// hasn't yet acknowledged it via its remote echo, the original content
86+
/// before redaction. Otherwise, None.
87+
pub(super) unredacted_content: Option<TimelineItemContent>,
8388
/// The kind of event timeline item, local or remote.
8489
pub(super) kind: EventTimelineItemKind,
8590
/// Whether or not the event belongs to an encrypted room.
@@ -125,6 +130,7 @@ impl EventTimelineItem {
125130
forwarder_profile: Option<TimelineDetails<Profile>>,
126131
timestamp: MilliSecondsSinceUnixEpoch,
127132
content: TimelineItemContent,
133+
unredacted_content: Option<TimelineItemContent>,
128134
kind: EventTimelineItemKind,
129135
is_room_encrypted: bool,
130136
) -> Self {
@@ -135,6 +141,7 @@ impl EventTimelineItem {
135141
forwarder_profile,
136142
timestamp,
137143
content,
144+
unredacted_content,
138145
kind,
139146
is_room_encrypted,
140147
}
@@ -478,7 +485,7 @@ impl EventTimelineItem {
478485
}
479486

480487
/// Create a clone of the current item, with content that's been redacted.
481-
pub(super) fn redact(&self, rules: &RedactionRules) -> Self {
488+
pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
482489
let content = self.content.redact(rules);
483490
let kind = match &self.kind {
484491
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
@@ -491,6 +498,26 @@ impl EventTimelineItem {
491498
forwarder_profile: self.forwarder_profile.clone(),
492499
timestamp: self.timestamp,
493500
content,
501+
unredacted_content: is_local.then_some(self.content.clone()),
502+
kind,
503+
is_room_encrypted: self.is_room_encrypted,
504+
}
505+
}
506+
507+
pub(super) fn unredact(&self) -> Self {
508+
let Some(content) = &self.unredacted_content else { return self.clone() };
509+
let kind = match &self.kind {
510+
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
511+
EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
512+
};
513+
Self {
514+
sender: self.sender.clone(),
515+
sender_profile: self.sender_profile.clone(),
516+
forwarder: self.forwarder.clone(),
517+
forwarder_profile: self.forwarder_profile.clone(),
518+
timestamp: self.timestamp,
519+
content: content.clone(),
520+
unredacted_content: None,
494521
kind,
495522
is_room_encrypted: self.is_room_encrypted,
496523
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ impl TestTimeline {
173173
txn_id
174174
}
175175

176+
async fn handle_local_redaction(&self, redacts: OwnedEventId) -> OwnedTransactionId {
177+
let txn_id = TransactionId::new();
178+
self.controller.handle_local_redaction(txn_id.clone(), redacts).await;
179+
txn_id
180+
}
181+
176182
async fn handle_back_paginated_event(&self, event: Raw<AnyTimelineEvent>) {
177183
let timeline_event = TimelineEvent::from_plaintext(event.cast());
178184
self.controller

crates/matrix-sdk-ui/src/timeline/tests/redaction.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use ruma::{
2323
StateEventContentChange, reaction::RedactedReactionEventContent,
2424
room::message::OriginalSyncRoomMessageEvent,
2525
},
26+
owned_event_id,
2627
};
2728
use stream_assert::{assert_next_matches, assert_pending};
2829

@@ -203,3 +204,34 @@ async fn test_reaction_redaction_timeline_filter() {
203204
assert_eq!(item.content().reactions().cloned().unwrap_or_default().len(), 0);
204205
assert_eq!(timeline.controller.items().await.len(), 2);
205206
}
207+
208+
#[async_test]
209+
async fn test_local_and_remote_echo_of_redaction() {
210+
let timeline = TestTimeline::new();
211+
let mut stream = timeline.subscribe_events().await;
212+
213+
let f = &timeline.factory;
214+
215+
// Send a message.
216+
let event_id = owned_event_id!("$1");
217+
timeline
218+
.handle_live_event(f.text_msg("Hello, world!").sender(&ALICE).event_id(&event_id))
219+
.await;
220+
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
221+
assert!(!item.content().is_redacted());
222+
assert!(item.unredacted_content.is_none());
223+
224+
// Now redact the message. We first emit the local echo of the redaction event.
225+
// The timeline event should be marked as being under redaction.
226+
timeline.handle_local_redaction(event_id.clone()).await;
227+
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
228+
assert!(item.content().is_redacted());
229+
assert!(item.unredacted_content.is_some());
230+
231+
// Then comes the remote echo of the redaction event. The timeline event should
232+
// now be redacted.
233+
timeline.handle_live_event(f.redaction(&event_id).sender(&ALICE)).await;
234+
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
235+
assert!(item.content().is_redacted());
236+
assert!(item.unredacted_content.is_none());
237+
}

0 commit comments

Comments
 (0)