Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bindings/matrix-sdk-ffi/src/timeline/msg_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
thread_summary,
}
}
Kind::Redacted => Self {
Kind::Redacted { unredacted_content: _ } => Self {
// TODO: Expose the unredacted content over FFI
kind: MsgLikeKind::Redacted,
reactions,
in_reply_to,
Expand Down
25 changes: 24 additions & 1 deletion crates/matrix-sdk-base/src/store/send_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ pub enum QueuedRequestKind {
#[serde(default)]
accumulated: Vec<AccumulatedSentMediaInfo>,
},

/// A redaction of another event to send.
Redaction {
/// The ID of the event to redact.
redacts: OwnedEventId,
/// The reason for the event being redacted.
reason: Option<String>,
},
}

impl From<SerializableEventContent> for QueuedRequestKind {
Expand Down Expand Up @@ -421,12 +429,27 @@ pub enum SentRequestKey {

/// The parent transaction returned an uploaded resource URL.
Media(SentMediaInfo),

/// The parent transaction returned a redaction event when it succeeded.
Redaction {
/// The event ID returned by the server.
event_id: OwnedEventId,

/// The ID of the redacted event.
redacts: OwnedEventId,

/// The reason for the event being redacted.
reason: Option<String>,
},
}

impl SentRequestKey {
/// Converts the current parent key into an event id, if possible.
pub fn into_event_id(self) -> Option<OwnedEventId> {
as_variant!(self, Self::Event { event_id, .. } => event_id)
match self {
Self::Event { event_id, .. } | Self::Redaction { event_id, .. } => Some(event_id),
_ => None,
}
}

/// Converts the current parent key into information about a sent media, if
Expand Down
2 changes: 2 additions & 0 deletions crates/matrix-sdk-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.

### Bug Fixes

- Handle local echoes of redactions in the timeline.
([#6250](https://github.com/matrix-org/matrix-rust-sdk/pull/6250))
- Don't show a "sent in clear" shield on live location timeline items in
encrypted rooms, since `beacon_info` is a state event that cannot be
encrypted by design.
Expand Down
38 changes: 28 additions & 10 deletions crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ pub(crate) enum AggregationKind {
},

/// An event has been redacted.
Redaction,
Redaction {
/// Whether this aggregation results from the local echo of a redaction.
/// Local echoes of redactions are applied reversibly whereas remote
/// echoes of redactions are applied irreversibly.
is_local: bool,
},

/// An event has been edited.
///
Expand Down Expand Up @@ -237,11 +242,13 @@ impl Aggregation {
}
}

AggregationKind::Redaction => {
if event.content().is_redacted() {
AggregationKind::Redaction { is_local } => {
let is_local_redacted = event.content().is_redacted_locally();
let is_remote_redacted = event.content().is_redacted() && !is_local_redacted;
if *is_local && is_local_redacted || !*is_local && is_remote_redacted {
ApplyAggregationResult::LeftItemIntact
} else {
let new_item = event.redact(&rules.redaction);
let new_item = event.redact(&rules.redaction, *is_local);
*event = Cow::Owned(new_item);
ApplyAggregationResult::UpdatedItem
}
Expand Down Expand Up @@ -352,9 +359,20 @@ impl Aggregation {
ApplyAggregationResult::Error(AggregationError::CantUndoPollEnd)
}

AggregationKind::Redaction => {
// Redactions are not reversible.
ApplyAggregationResult::Error(AggregationError::CantUndoRedaction)
AggregationKind::Redaction { is_local } => {
if *is_local {
if event.content().is_redacted_locally() {
// Unapply local redaction.
*event = Cow::Owned(event.unredact());
ApplyAggregationResult::UpdatedItem
} else {
// Event isn't locally redacted. Nothing to do.
ApplyAggregationResult::LeftItemIntact
}
} else {
// Remote redactions are not reversible.
ApplyAggregationResult::Error(AggregationError::CantUndoRedaction)
}
}

AggregationKind::Reaction { key, sender, .. } => {
Expand Down Expand Up @@ -477,7 +495,7 @@ impl Aggregations {
pub fn add(&mut self, related_to: TimelineEventItemId, aggregation: Aggregation) {
// If the aggregation is a redaction, it invalidates all the other aggregations;
// remove them.
if matches!(aggregation.kind, AggregationKind::Redaction) {
if matches!(aggregation.kind, AggregationKind::Redaction { .. }) {
for agg in self.related_events.remove(&related_to).unwrap_or_default() {
self.inverted_map.remove(&agg.own_id);
}
Expand All @@ -488,7 +506,7 @@ impl Aggregations {
if let Some(previous_aggregations) = self.related_events.get(&related_to)
&& previous_aggregations
.iter()
.any(|agg| matches!(agg.kind, AggregationKind::Redaction))
.any(|agg| matches!(agg.kind, AggregationKind::Redaction { .. }))
{
return;
}
Expand Down Expand Up @@ -698,7 +716,7 @@ impl Aggregations {
AggregationKind::PollResponse { .. }
| AggregationKind::PollEnd { .. }
| AggregationKind::Edit(..)
| AggregationKind::Redaction
| AggregationKind::Redaction { .. }
| AggregationKind::BeaconUpdate { .. }
| AggregationKind::BeaconStop { .. } => {
// Nothing particular to do.
Expand Down
45 changes: 45 additions & 0 deletions crates/matrix-sdk-ui/src/timeline/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,23 @@ impl<P: RoomDataProvider> TimelineController<P> {
LocalEchoContent::React { key, send_handle, applies_to } => {
self.handle_local_reaction(key, send_handle, applies_to).await;
}

LocalEchoContent::Redaction { redacts, send_error, .. } => {
self.handle_local_redaction(echo.transaction_id.clone(), redacts).await;

if let Some(send_error) = send_error {
self.update_event_send_state(
&echo.transaction_id,
EventSendState::SendingFailed {
error: Arc::new(matrix_sdk::Error::SendQueueWedgeError(Box::new(
send_error,
))),
is_recoverable: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this bool be based on a field of send_error somehow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I think not. This is actually copied from the LocalEchoContent::Event match-arm a few lines above. It seems that send_error is of type QueueWedgeError which appears to always be unrecoverable.

/// Represents a failed to send unrecoverable error of an event sent via the
/// send queue.

},
)
.await;
}
}
}
}

Expand Down Expand Up @@ -1308,6 +1325,34 @@ impl<P: RoomDataProvider> TimelineController<P> {
tr.commit();
}

/// Applies a local echo of a redaction.
pub(super) async fn handle_local_redaction(
&self,
txn_id: OwnedTransactionId,
redacts: OwnedEventId,
) {
let mut state = self.state.write().await;
let mut tr = state.transaction();

let target = TimelineEventItemId::EventId(redacts);

let aggregation = Aggregation::new(
TimelineEventItemId::TransactionId(txn_id),
AggregationKind::Redaction { is_local: true },
);

tr.meta.aggregations.add(target.clone(), aggregation.clone());
find_item_and_apply_aggregation(
&tr.meta.aggregations,
&mut tr.items,
&target,
aggregation,
&tr.meta.room_version_rules,
);

tr.commit();
}

/// Handle a single room send queue update.
pub(crate) async fn handle_room_send_queue_update(&self, update: RoomSendQueueUpdate) {
match update {
Expand Down
8 changes: 6 additions & 2 deletions crates/matrix-sdk-ui/src/timeline/event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -845,8 +845,12 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
}

let target = TimelineEventItemId::EventId(redacted.clone());
let aggregation =
Aggregation::new(self.ctx.flow.timeline_item_id(), AggregationKind::Redaction);
let aggregation = Aggregation::new(
self.ctx.flow.timeline_item_id(),
AggregationKind::Redaction {
is_local: false, // We can only get here for remote echoes of redactions.
},
);
self.meta.aggregations.add(target.clone(), aggregation.clone());

find_item_and_apply_aggregation(
Expand Down
48 changes: 44 additions & 4 deletions crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,22 @@ impl TimelineItemContent {
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(_), .. }))
}

/// Whether the underlying event was redacted (either locally or remotely).
pub fn is_redacted(&self) -> bool {
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted { .. }, .. }))
}

/// Whether the underlying event was redacted locally. This means the
/// redaction was sent to the server but we're still waiting on the
/// server to acknowledge it with its remote echo.
pub fn is_redacted_locally(&self) -> bool {
matches!(
self,
Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Redacted { unredacted_content: Some(_) },
..
})
)
}

// These constructors could also be `From` implementations, but that would
Expand Down Expand Up @@ -325,10 +339,22 @@ impl TimelineItemContent {
}
}

pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
pub(in crate::timeline) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
match self {
Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we should handle the other match arms below, too. I'm not sure how to apply MsgLikeKind::Redacted to them though given that they currently use variants other than TimelineItemContent::MsgLike.

TimelineItemContent::MsgLike(MsgLikeContent::redacted())
if is_local {
TimelineItemContent::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Redacted {
unredacted_content: Some(Box::new(self.clone())),
},
reactions: Default::default(),
thread_root: None,
in_reply_to: None,
thread_summary: None,
})
} else {
TimelineItemContent::MsgLike(MsgLikeContent::redacted())
}
}
Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
Expand All @@ -337,6 +363,20 @@ impl TimelineItemContent {
}
}

/// Create a clone of the current item, with content restored from the
/// item's unredacted_content (if it was previously set by a call to
/// the `redact(...)` method).
pub(in crate::timeline) fn unredact(&self) -> Self {
let Self::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Redacted { unredacted_content: Some(content) },
..
}) = self
else {
return self.clone();
};
*content.clone()
}

/// Event ID of the thread root, if this is a message in a thread.
pub fn thread_root(&self) -> Option<OwnedEventId> {
as_variant!(self, Self::MsgLike)?.thread_root.clone()
Expand Down Expand Up @@ -890,7 +930,7 @@ mod tests {
change: Some(MembershipChange::Banned),
});

let redacted = content.redact(&RedactionRules::V11);
let redacted = content.redact(&RedactionRules::V11, false);
assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
assert_eq!(inner.change, Some(MembershipChange::Banned));
assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ use super::{
Sticker,
};
use crate::timeline::{
ReactionsByKeyBySender, TimelineDetails, event_item::content::other::OtherMessageLike,
ReactionsByKeyBySender, TimelineDetails, TimelineItemContent,
event_item::content::other::OtherMessageLike,
};

#[derive(Clone, Debug)]
Expand All @@ -35,7 +36,12 @@ pub enum MsgLikeKind {
Poll(PollState),

/// A redacted message.
Redacted,
Redacted {
/// If a redaction for this event is currently being sent but the server
/// hasn't yet acknowledged it via its remote echo, the original content
/// before redaction. Otherwise, None.
unredacted_content: Option<Box<TimelineItemContent>>,
},

/// An `m.room.encrypted` event that could not be decrypted.
UnableToDecrypt(EncryptedMessage),
Expand Down Expand Up @@ -90,7 +96,7 @@ impl MsgLikeContent {
MsgLikeKind::Message(_) => "a message",
MsgLikeKind::Sticker(_) => "a sticker",
MsgLikeKind::Poll(_) => "a poll",
MsgLikeKind::Redacted => "a redacted message",
MsgLikeKind::Redacted { .. } => "a redacted message",
MsgLikeKind::UnableToDecrypt(_) => "an encrypted message we couldn't decrypt",
MsgLikeKind::Other(_) => "a custom message-like event",
MsgLikeKind::LiveLocation(_) => "a live location share",
Expand All @@ -99,7 +105,7 @@ impl MsgLikeContent {

pub fn redacted() -> Self {
Self {
kind: MsgLikeKind::Redacted,
kind: MsgLikeKind::Redacted { unredacted_content: None },
reactions: Default::default(),
thread_root: None,
in_reply_to: None,
Expand Down
29 changes: 25 additions & 4 deletions crates/matrix-sdk-ui/src/timeline/event_item/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ pub struct EventTimelineItem {
pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
/// The timestamp of the event.
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
/// The content of the event.
/// The content of the event. Might be redacted if a redaction for this
/// event is currently being sent or has been received from the server.
pub(super) content: TimelineItemContent,
/// The kind of event timeline item, local or remote.
pub(super) kind: EventTimelineItemKind,
Expand Down Expand Up @@ -506,8 +507,8 @@ impl EventTimelineItem {
}

/// Create a clone of the current item, with content that's been redacted.
pub(super) fn redact(&self, rules: &RedactionRules) -> Self {
let content = self.content.redact(rules);
pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
let content = self.content.redact(rules, is_local);
let kind = match &self.kind {
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
Expand All @@ -524,6 +525,26 @@ impl EventTimelineItem {
}
}

/// Create a clone of the current item, with content restored from the
/// item's unredacted_content field (if it was previously set by a call to
/// the `redact(...)` method).
pub(super) fn unredact(&self) -> Self {
let kind = match &self.kind {
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
};
Self {
sender: self.sender.clone(),
sender_profile: self.sender_profile.clone(),
forwarder: self.forwarder.clone(),
forwarder_profile: self.forwarder_profile.clone(),
timestamp: self.timestamp,
content: self.content.unredact(),
kind,
is_room_encrypted: self.is_room_encrypted,
}
}

pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
match &self.kind {
EventTimelineItemKind::Local(local) => {
Expand Down Expand Up @@ -580,7 +601,7 @@ impl EventTimelineItem {
},
MsgLikeKind::Sticker(_)
| MsgLikeKind::Poll(_)
| MsgLikeKind::Redacted
| MsgLikeKind::Redacted { .. }
| MsgLikeKind::UnableToDecrypt(_)
| MsgLikeKind::Other(_)
| MsgLikeKind::LiveLocation(_) => None,
Expand Down
Loading
Loading