Skip to content

Commit 01a4c00

Browse files
committed
feat(send_queue): send redactions via the send queue
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
1 parent f49784a commit 01a4c00

File tree

14 files changed

+351
-13
lines changed

14 files changed

+351
-13
lines changed

crates/matrix-sdk-base/src/store/send_queue.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ pub enum QueuedRequestKind {
117117
#[serde(default)]
118118
accumulated: Vec<AccumulatedSentMediaInfo>,
119119
},
120+
121+
/// A redaction of another event to send.
122+
Redaction {
123+
/// The ID of the event to redact.
124+
redacts: OwnedEventId,
125+
/// The reason for the event being redacted.
126+
reason: Option<String>,
127+
},
120128
}
121129

122130
impl From<SerializableEventContent> for QueuedRequestKind {
@@ -421,12 +429,28 @@ pub enum SentRequestKey {
421429

422430
/// The parent transaction returned an uploaded resource URL.
423431
Media(SentMediaInfo),
432+
433+
/// The parent transaction returned a redaction event when it succeeded.
434+
Redaction {
435+
/// The event ID returned by the server.
436+
event_id: OwnedEventId,
437+
438+
/// The ID of the redacted event.
439+
redacts: OwnedEventId,
440+
441+
/// The reason for the event being redacted.
442+
reason: Option<String>,
443+
},
424444
}
425445

426446
impl SentRequestKey {
427447
/// Converts the current parent key into an event id, if possible.
428448
pub fn into_event_id(self) -> Option<OwnedEventId> {
429-
as_variant!(self, Self::Event { event_id, .. } => event_id)
449+
match self {
450+
Self::Event { event_id, .. } => Some(event_id),
451+
Self::Redaction { event_id, .. } => Some(event_id),
452+
_ => None,
453+
}
430454
}
431455

432456
/// Converts the current parent key into information about a sent media, if

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

Lines changed: 30 additions & 9 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,8 +242,15 @@ impl Aggregation {
237242
}
238243
}
239244

240-
AggregationKind::Redaction => {
241-
if event.content().is_redacted() {
245+
AggregationKind::Redaction { is_local } => {
246+
if *is_local {
247+
if event.is_being_redacted {
248+
ApplyAggregationResult::LeftItemIntact
249+
} else {
250+
*event = Cow::Owned(event.with_is_being_redacted(true));
251+
ApplyAggregationResult::UpdatedItem
252+
}
253+
} else if event.content().is_redacted() {
242254
ApplyAggregationResult::LeftItemIntact
243255
} else {
244256
let new_item = event.redact(&rules.redaction);
@@ -352,9 +364,18 @@ impl Aggregation {
352364
ApplyAggregationResult::Error(AggregationError::CantUndoPollEnd)
353365
}
354366

355-
AggregationKind::Redaction => {
356-
// Redactions are not reversible.
357-
ApplyAggregationResult::Error(AggregationError::CantUndoRedaction)
367+
AggregationKind::Redaction { is_local } => {
368+
if *is_local {
369+
if event.is_being_redacted {
370+
*event = Cow::Owned(event.with_is_being_redacted(false));
371+
ApplyAggregationResult::UpdatedItem
372+
} else {
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
@@ -241,6 +241,7 @@ mod tests {
241241
TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
242242
event_kind,
243243
true,
244+
false,
244245
)),
245246
TimelineUniqueId("local".to_owned()),
246247
)
@@ -284,6 +285,7 @@ mod tests {
284285
)),
285286
event_kind,
286287
true,
288+
false,
287289
)),
288290
TimelineUniqueId("local".to_owned()),
289291
)
@@ -331,6 +333,7 @@ mod tests {
331333
),
332334
event_kind,
333335
true,
336+
false,
334337
)),
335338
TimelineUniqueId("local".to_owned()),
336339
)

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,23 @@ impl<P: RoomDataProvider> TimelineController<P> {
12681268
LocalEchoContent::React { key, send_handle, applies_to } => {
12691269
self.handle_local_reaction(key, send_handle, applies_to).await;
12701270
}
1271+
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+
}
1287+
}
12711288
}
12721289
}
12731290

@@ -1308,6 +1325,34 @@ impl<P: RoomDataProvider> TimelineController<P> {
13081325
tr.commit();
13091326
}
13101327

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+
13111356
/// Handle a single room send queue update.
13121357
pub(crate) async fn handle_room_send_queue_update(&self, update: RoomSendQueueUpdate) {
13131358
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
@@ -760,6 +760,7 @@ mod observable_items_tests {
760760
origin: RemoteEventOrigin::Sync,
761761
}),
762762
false,
763+
false,
763764
),
764765
TimelineUniqueId(format!("__eid_{event_id}")),
765766
)
@@ -790,6 +791,7 @@ mod observable_items_tests {
790791
send_handle: None,
791792
}),
792793
false,
794+
false,
793795
),
794796
TimelineUniqueId(format!("__tid_{transaction_id}")),
795797
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@ mod tests {
689689
TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
690690
event_kind,
691691
false,
692+
false,
692693
)
693694
}
694695

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,8 +830,13 @@ 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+
// We can only get here for remote echoes of redactions.
837+
is_local: false,
838+
},
839+
);
835840
self.meta.aggregations.add(target.clone(), aggregation.clone());
836841

837842
find_item_and_apply_aggregation(
@@ -953,6 +958,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
953958
content,
954959
kind,
955960
is_room_encrypted,
961+
false,
956962
);
957963

958964
// Apply any pending or stashed aggregations.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub struct EventTimelineItem {
8686
///
8787
/// May be false when we don't know about the room encryption status yet.
8888
pub(super) is_room_encrypted: bool,
89+
/// Whether a redaction for this event is currently being sent.
90+
pub(super) is_being_redacted: bool,
8991
}
9092

9193
#[derive(Clone, Debug)]
@@ -127,6 +129,7 @@ impl EventTimelineItem {
127129
content: TimelineItemContent,
128130
kind: EventTimelineItemKind,
129131
is_room_encrypted: bool,
132+
is_being_redacted: bool,
130133
) -> Self {
131134
Self {
132135
sender,
@@ -137,6 +140,7 @@ impl EventTimelineItem {
137140
content,
138141
kind,
139142
is_room_encrypted,
143+
is_being_redacted,
140144
}
141145
}
142146

@@ -493,9 +497,15 @@ impl EventTimelineItem {
493497
content,
494498
kind,
495499
is_room_encrypted: self.is_room_encrypted,
500+
is_being_redacted: false,
496501
}
497502
}
498503

504+
/// Clone the current event item, and update its `is_being_redacted`.
505+
pub(super) fn with_is_being_redacted(&self, is_being_redacted: bool) -> Self {
506+
Self { is_being_redacted, ..self.clone() }
507+
}
508+
499509
pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
500510
match &self.kind {
501511
EventTimelineItemKind::Local(local) => {

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.is_being_redacted);
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.is_being_redacted);
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.is_being_redacted);
237+
}

0 commit comments

Comments
 (0)