From 0a5a22ec6fcf0546cd57b61b400d95b00da60e07 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Tue, 6 Jan 2026 11:49:11 +0000 Subject: [PATCH] feat(ui): Expose information about room key bundle forwarder. Signed-off-by: Skye Elliot --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 19 +++++++++ .../controller/decryption_retry_task.rs | 6 +++ .../timeline/controller/observable_items.rs | 4 ++ .../src/timeline/controller/state.rs | 2 + .../timeline/controller/state_transaction.rs | 28 +++++++++++++ .../src/timeline/date_dividers.rs | 2 + .../src/timeline/event_handler.rs | 20 +++++++++ .../src/timeline/event_item/mod.rs | 42 ++++++++++++++++++- 8 files changed, 122 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 9ff9ebe99..cc36c0e2e 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1007,6 +1007,8 @@ pub struct EventTimelineItem { event_or_transaction_id: EventOrTransactionId, sender: String, sender_profile: ProfileDetails, + forwarder: Option, + forwarder_profile: Option, is_own: bool, is_editable: bool, content: TimelineItemContent, @@ -1030,6 +1032,8 @@ impl From for EventTimelineItem { event_or_transaction_id: item.identifier().into(), sender: item.sender().to_string(), sender_profile: item.sender_profile().clone().into(), + forwarder: item.forwarder().map(ToString::to_string), + forwarder_profile: item.forwarder_profile().map(Into::into), is_own: item.is_own(), is_editable: item.is_editable(), content: item.content().clone().into(), @@ -1085,6 +1089,21 @@ impl From> for ProfileDetails { } } +impl From<&TimelineDetails> for ProfileDetails { + fn from(details: &TimelineDetails) -> Self { + match details { + TimelineDetails::Unavailable => Self::Unavailable, + TimelineDetails::Pending => Self::Pending, + TimelineDetails::Ready(profile) => Self::Ready { + display_name: profile.display_name.clone(), + display_name_ambiguous: profile.display_name_ambiguous, + avatar_url: profile.avatar_url.as_ref().map(ToString::to_string), + }, + TimelineDetails::Error(e) => Self::Error { message: e.to_string() }, + } + } +} + #[derive(Clone, uniffi::Record)] pub struct PollData { question: String, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs index 68fa53bb0..c88c82956 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs @@ -236,6 +236,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::MsgLike(MsgLikeContent::redacted()), event_kind, @@ -262,6 +264,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::MsgLike(MsgLikeContent::unable_to_decrypt( EncryptedMessage::from_content( @@ -315,6 +319,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::message( content.msgtype, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 409679fdb..76735ffb4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -734,6 +734,8 @@ mod observable_items_tests { EventTimelineItem::new( owned_user_id!("@ivan:mnt.io"), TimelineDetails::Unavailable, + None, + None, MilliSecondsSinceUnixEpoch(0u32.into()), TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(Message { @@ -768,6 +770,8 @@ mod observable_items_tests { EventTimelineItem::new( owned_user_id!("@ivan:mnt.io"), TimelineDetails::Unavailable, + None, + None, MilliSecondsSinceUnixEpoch(0u32.into()), TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(Message { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index ba0e813b3..47838fb5a 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -180,6 +180,8 @@ impl TimelineState

{ let ctx = TimelineEventContext { sender: own_user_id, sender_profile: own_profile, + forwarder: None, + forwarder_profile: None, timestamp: MilliSecondsSinceUnixEpoch::now(), read_receipts: Default::default(), // An event sent by ourselves is never matched against push rules. diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs index 5c257d69e..14251e44e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs @@ -226,9 +226,23 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { | Some(action @ TimelineAction::HandleAggregation { .. }) => { let encryption_info = event.kind.encryption_info().cloned(); let sender_profile = room_data_provider.profile_from_user_id(&sender).await; + + let forwarder = encryption_info + .as_ref() + .and_then(|info| info.forwarder.as_ref()) + .map(|info| info.user_id.clone()); + + let forwarder_profile = if let Some(ref forwarder_id) = forwarder { + Some(room_data_provider.profile_from_user_id(forwarder_id).await) + } else { + None + }; + let mut ctx = TimelineEventContext { sender, sender_profile, + forwarder, + forwarder_profile: forwarder_profile.flatten(), timestamp, // These are not used when handling an aggregation. read_receipts: Default::default(), @@ -700,6 +714,18 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { map.get(&UnsignedEventLocation::RelationsReplace)?.encryption_info().cloned() }); + let forwarder = event + .kind + .encryption_info() + .and_then(|info| info.forwarder.as_ref()) + .map(|info| info.user_id.clone()); + + let forwarder_profile = if let Some(ref forwarder_id) = forwarder { + Some(room_data_provider.profile_from_user_id(forwarder_id).await) + } else { + None + }; + let (raw, utd_info) = match event.kind { TimelineEventKind::UnableToDecrypt { utd_info, event } => (event, Some(utd_info)), _ => (event.kind.into_raw(), None), @@ -794,6 +820,8 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { let ctx = TimelineEventContext { sender, sender_profile, + forwarder, + forwarder_profile: forwarder_profile.flatten(), timestamp, read_receipts: if settings.track_read_receipts.is_enabled() && should_add diff --git a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs index 54d766220..6d7b4d842 100644 --- a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs @@ -683,6 +683,8 @@ mod tests { EventTimelineItem::new( owned_user_id!("@alice:example.org"), crate::timeline::TimelineDetails::Pending, + None, + None, timestamp, TimelineItemContent::MsgLike(MsgLikeContent::redacted()), event_kind, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 454fe9660..48f93dcc7 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -108,6 +108,16 @@ impl Flow { pub(super) struct TimelineEventContext { pub(super) sender: OwnedUserId, pub(super) sender_profile: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the forwarder's profile. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder_profile: Option, /// The event's `origin_server_ts` field (or creation time for local echo). pub(super) timestamp: MilliSecondsSinceUnixEpoch, pub(super) read_receipts: IndexMap, @@ -762,6 +772,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { fn add_item(&mut self, content: TimelineItemContent) { let sender = self.ctx.sender.to_owned(); let sender_profile = TimelineDetails::from_initial_value(self.ctx.sender_profile.clone()); + + let forwarder = self.ctx.forwarder.to_owned(); + let forwarder_profile = self + .ctx + .forwarder + .as_ref() + .map(|_| TimelineDetails::from_initial_value(self.ctx.forwarder_profile.clone())); + let timestamp = self.ctx.timestamp; let kind: EventTimelineItemKind = match &self.ctx.flow { @@ -808,6 +826,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let item = EventTimelineItem::new( sender, sender_profile, + forwarder, + forwarder_profile, timestamp, content, kind, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 09f48b9d6..e2afdb31b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -67,6 +67,16 @@ pub struct EventTimelineItem { pub(super) sender: OwnedUserId, /// The sender's profile of the event. pub(super) sender_profile: TimelineDetails, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the forwarder's profile, if present. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder_profile: Option>, /// The timestamp of the event. pub(super) timestamp: MilliSecondsSinceUnixEpoch, /// The content of the event. @@ -108,15 +118,27 @@ pub(crate) enum TimelineItemHandle<'a> { } impl EventTimelineItem { + #[allow(clippy::too_many_arguments)] pub(super) fn new( sender: OwnedUserId, sender_profile: TimelineDetails, + forwarder: Option, + forwarder_profile: Option>, timestamp: MilliSecondsSinceUnixEpoch, content: TimelineItemContent, kind: EventTimelineItemKind, is_room_encrypted: bool, ) -> Self { - Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted } + Self { + sender, + sender_profile, + forwarder, + forwarder_profile, + timestamp, + content, + kind, + is_room_encrypted, + } } /// Check whether this item is a local echo. @@ -216,6 +238,22 @@ impl EventTimelineItem { &self.sender_profile } + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, returns the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub fn forwarder(&self) -> Option<&UserId> { + self.forwarder.as_deref() + } + + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, returns the profile of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub fn forwarder_profile(&self) -> Option<&TimelineDetails> { + self.forwarder_profile.as_ref() + } + /// Get the content of this item. pub fn content(&self) -> &TimelineItemContent { &self.content @@ -449,6 +487,8 @@ impl EventTimelineItem { 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, kind,