diff --git a/bindings/matrix-sdk-ffi/src/room/mod.rs b/bindings/matrix-sdk-ffi/src/room/mod.rs index 4dd0d2726..c10d09516 100644 --- a/bindings/matrix-sdk-ffi/src/room/mod.rs +++ b/bindings/matrix-sdk-ffi/src/room/mod.rs @@ -233,7 +233,8 @@ impl Room { builder = builder .with_focus(configuration.focus.try_into()?) - .with_date_divider_mode(configuration.date_divider_mode.into()); + .with_date_divider_mode(configuration.date_divider_mode.into()) + .state_events_can_show_read_receipts(configuration.state_events_can_show_read_receipts); if configuration.track_read_receipts { builder = builder.track_read_marker_and_receipts(); diff --git a/bindings/matrix-sdk-ffi/src/timeline/configuration.rs b/bindings/matrix-sdk-ffi/src/timeline/configuration.rs index 17301836a..808741023 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/configuration.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/configuration.rs @@ -164,6 +164,9 @@ pub struct TimelineConfiguration { /// How should we filter out events from the timeline? pub filter: TimelineFilter, + /// Can read receipts be shown on state events or only on messages? + pub state_events_can_show_read_receipts: bool, + /// An optional String that will be prepended to /// all the timeline item's internal IDs, making it possible to /// distinguish different timeline instances from each other. diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index f139b3097..4fccd710a 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -141,6 +141,11 @@ impl TimelineBuilder { self } + pub fn state_events_can_show_read_receipts(mut self, show: bool) -> Self { + self.settings.state_events_can_show_read_receipts = show; + self + } + /// Whether to add events that failed to deserialize to the timeline. /// /// Defaults to `true`. diff --git a/crates/matrix-sdk-ui/src/timeline/controller/metadata.rs b/crates/matrix-sdk-ui/src/timeline/controller/metadata.rs index c18906702..e0b4f2fd0 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/metadata.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/metadata.rs @@ -519,6 +519,9 @@ pub(in crate::timeline) struct EventMeta { /// Whether the event is among the timeline items. pub visible: bool, + /// Whether the event can show read receipts. + pub can_show_read_receipts: bool, + /// Foundation for the mapping between remote events to timeline items. /// /// Let's explain it. The events represent the first set and are stored in @@ -587,8 +590,15 @@ impl EventMeta { pub fn new( event_id: OwnedEventId, visible: bool, + can_show_read_receipts: bool, thread_root_id: Option, ) -> Self { - Self { event_id, thread_root_id, visible, timeline_item_index: None } + Self { + event_id, + thread_root_id, + visible, + can_show_read_receipts, + timeline_item_index: None, + } } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index b4e406bd9..cc721ec7d 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -268,6 +268,9 @@ pub(super) struct TimelineSettings { /// Should the read receipts and read markers be handled? pub(super) track_read_receipts: bool, + /// Whether state events can show read receipts. + pub(super) state_events_can_show_read_receipts: bool, + /// Event filter that controls what's rendered as a timeline item (and thus /// what can carry read receipts). pub(super) event_filter: Arc, @@ -284,6 +287,7 @@ impl fmt::Debug for TimelineSettings { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TimelineSettings") .field("track_read_receipts", &self.track_read_receipts) + .field("state_events_can_show_read_receipts", &self.state_events_can_show_read_receipts) .field("add_failed_to_parse", &self.add_failed_to_parse) .finish_non_exhaustive() } @@ -293,6 +297,7 @@ impl Default for TimelineSettings { fn default() -> Self { Self { track_read_receipts: false, + state_events_can_show_read_receipts: true, event_filter: Arc::new(default_event_filter), add_failed_to_parse: true, date_divider_mode: DateDividerMode::Daily, 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 e889b01a5..409679fdb 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -801,6 +801,7 @@ mod observable_items_tests { thread_root_id: None, timeline_item_index: None, visible: false, + can_show_read_receipts: false, } } @@ -2065,6 +2066,7 @@ mod all_remote_events_tests { thread_root_id: None, timeline_item_index, visible: false, + can_show_read_receipts: false, } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs index 2dc9330b4..013dfc509 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs @@ -131,8 +131,13 @@ impl ReadReceipts { old_receipt_pos = Some(pos); } - // The receipt should appear on the first event that is visible. - if old_receipt_pos.is_some() && old_item_event_id.is_none() && event.visible { + // The receipt should appear on the first visible event that can show read + // receipts. + if old_receipt_pos.is_some() + && old_item_event_id.is_none() + && event.visible + && event.can_show_read_receipts + { old_item_pos = event.timeline_item_index; old_item_event_id = Some(event.event_id.clone()); } @@ -141,8 +146,13 @@ impl ReadReceipts { new_receipt_pos = Some(pos); } - // The receipt should appear on the first event that is visible. - if new_receipt_pos.is_some() && new_item_event_id.is_none() && event.visible { + // The receipt should appear on the first visible event that can show read + // receipts. + if new_receipt_pos.is_some() + && new_item_event_id.is_none() + && event.visible + && event.can_show_read_receipts + { new_item_pos = event.timeline_item_index; new_item_event_id = Some(event.event_id.clone()); } @@ -316,11 +326,16 @@ impl ReadReceipts { } } - // Include receipts for all the following non-visible events. + // Include receipts from all the following events that are hidden or can't show + // read receipts. let mut hidden = Vec::new(); - for hidden_event_meta in events_iter.take_while(|meta| !meta.visible) { - if let Some(event_receipts) = self.get_event_receipts(&hidden_event_meta.event_id) { - trace!(%hidden_event_meta.event_id, "found receipts on hidden event"); + for hidden_receipt_event_meta in + events_iter.take_while(|meta| !meta.visible || !meta.can_show_read_receipts) + { + if let Some(event_receipts) = + self.get_event_receipts(&hidden_receipt_event_meta.event_id) + { + trace!(%hidden_receipt_event_meta.event_id, "found receipts on hidden event"); hidden.extend(event_receipts.clone()); } } @@ -659,8 +674,8 @@ impl TimelineStateTransaction<'_, P> { .skip_while(|meta| meta.event_id != event_id) // Go past the event item. .skip(1) - // Find the first visible item. - .find(|meta| meta.visible) + // Find the first visible item that can show read receipts. + .find(|meta| meta.visible && meta.can_show_read_receipts) else { trace!("Couldn't find any previous visible event, exiting"); return; @@ -806,7 +821,7 @@ impl TimelineState

{ .iter() .rev() .skip_while(|ev| ev.event_id != *latest_receipt_id) - .find(|ev| ev.visible) + .find(|ev| ev.visible && ev.can_show_read_receipts) .map(|ev| ev.event_id.clone()) } } 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 db4d5cf0d..25f590616 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs @@ -460,6 +460,19 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { } } + /// Whether this event can show read receipts, or if they should be moved + /// to the previous event. + fn can_show_read_receipts( + &self, + settings: &TimelineSettings, + event: &AnySyncTimelineEvent, + ) -> bool { + match event { + AnySyncTimelineEvent::State(_) => settings.state_events_can_show_read_receipts, + AnySyncTimelineEvent::MessageLike(_) => true, + } + } + /// After a deserialization error, adds a failed-to-parse item to the /// timeline if configured to do so, or logs the error (and optionally /// save metadata) if not. @@ -478,6 +491,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { Option, Option, bool, + bool, )> { let state_key: Option = raw.get_field("state_key").ok().flatten(); @@ -537,6 +551,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { Some(TimelineAction::failed_to_parse(event_type, deserialization_error)), None, true, + true, )) } @@ -552,7 +567,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { // Remember the event before returning prematurely. // See [`ObservableItems::all_remote_events`]. self.add_or_update_remote_event( - EventMeta::new(event_id, false, None), + EventMeta::new(event_id, false, false, None), sender.as_deref(), origin_server_ts, position, @@ -630,65 +645,74 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { _ => (event.kind.into_raw(), None), }; - let (event_id, sender, timestamp, txn_id, timeline_action, thread_root, should_add) = - match raw.deserialize() { - // Classical path: the event is valid, can be deserialized, everything is alright. - Ok(event) => { - let (in_reply_to, thread_root) = self.meta.process_event_relations( - &event, + let ( + event_id, + sender, + timestamp, + txn_id, + timeline_action, + thread_root, + should_add, + can_show_read_receipts, + ) = match raw.deserialize() { + // Classical path: the event is valid, can be deserialized, everything is alright. + Ok(event) => { + let (in_reply_to, thread_root) = self.meta.process_event_relations( + &event, + &raw, + bundled_edit_encryption_info, + &self.items, + self.focus.is_thread(), + ); + + let should_add = self.should_add_event_item( + room_data_provider, + settings, + &event, + thread_root.as_deref(), + position, + ); + + let can_show_read_receipts = self.can_show_read_receipts(settings, &event); + + ( + event.event_id().to_owned(), + event.sender().to_owned(), + event.origin_server_ts(), + event.transaction_id().map(ToOwned::to_owned), + TimelineAction::from_event( + event, &raw, - bundled_edit_encryption_info, - &self.items, - self.focus.is_thread(), - ); - - let should_add = self.should_add_event_item( room_data_provider, - settings, - &event, - thread_root.as_deref(), - position, - ); - - ( - event.event_id().to_owned(), - event.sender().to_owned(), - event.origin_server_ts(), - event.transaction_id().map(ToOwned::to_owned), - TimelineAction::from_event( - event, - &raw, - room_data_provider, - utd_info.map(|utd_info| { - (utd_info, self.meta.unable_to_decrypt_hook.as_ref()) - }), - in_reply_to, - thread_root.clone(), - thread_summary, - ) - .await, - thread_root, - should_add, + utd_info + .map(|utd_info| (utd_info, self.meta.unable_to_decrypt_hook.as_ref())), + in_reply_to, + thread_root.clone(), + thread_summary, ) - } + .await, + thread_root, + should_add, + can_show_read_receipts, + ) + } - // The event seems invalid… - Err(e) => { - if let Some(tuple) = self - .maybe_add_error_item(position, room_data_provider, &raw, e, settings) - .await - { - tuple - } else { - return false; - } + // The event seems invalid… + Err(e) => { + if let Some(tuple) = + self.maybe_add_error_item(position, room_data_provider, &raw, e, settings).await + { + tuple + } else { + return false; } - }; + } + }; // Remember the event. // See [`ObservableItems::all_remote_events`]. self.add_or_update_remote_event( - EventMeta::new(event_id.clone(), should_add, thread_root), + EventMeta::new(event_id.clone(), should_add, can_show_read_receipts, thread_root), Some(&sender), Some(timestamp), position, @@ -711,7 +735,10 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { sender, sender_profile, timestamp, - read_receipts: if settings.track_read_receipts && should_add { + read_receipts: if settings.track_read_receipts + && should_add + && can_show_read_receipts + { self.meta.read_receipts.compute_event_receipts( &event_id, &mut self.items, @@ -887,9 +914,11 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { TimelineItemPosition::UpdateAt { .. } => { if let Some(event) = self.items.get_remote_event_by_event_id_mut(&event_meta.event_id) - && event.visible != event_meta.visible + && (event.visible != event_meta.visible + || event.can_show_read_receipts != event_meta.can_show_read_receipts) { event.visible = event_meta.visible; + event.can_show_read_receipts = event_meta.can_show_read_receipts; if settings.track_read_receipts { // Since the event's visibility changed, we need to update the read diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index 00ce8eee6..bf8caf395 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -98,7 +98,11 @@ async fn test_replace_with_initial_events_and_read_marker() { .with_fully_read_marker(event_id) .with_initial_user_receipts(receipts), ) - .settings(TimelineSettings { track_read_receipts: true, ..Default::default() }) + .settings(TimelineSettings { + track_read_receipts: true, + state_events_can_show_read_receipts: true, + ..Default::default() + }) .build(); let f = &timeline.factory; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index 3611e0a0b..9afd42774 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -239,7 +239,11 @@ async fn test_no_read_marker_with_local_echo() { let timeline = TestTimelineBuilder::new() .provider(TestRoomDataProvider::default().with_fully_read_marker(event_id.to_owned())) - .settings(TimelineSettings { track_read_receipts: true, ..Default::default() }) + .settings(TimelineSettings { + track_read_receipts: true, + state_events_can_show_read_receipts: true, + ..Default::default() + }) .build(); let f = &timeline.factory; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs b/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs index e7fef6b30..f8bd669eb 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs @@ -97,7 +97,11 @@ async fn test_default_filter() { #[async_test] async fn test_filter_always_false() { let timeline = TestTimelineBuilder::new() - .settings(TimelineSettings { event_filter: Arc::new(|_, _| false), ..Default::default() }) + .settings(TimelineSettings { + event_filter: Arc::new(|_, _| false), + state_events_can_show_read_receipts: true, + ..Default::default() + }) .build(); let f = &timeline.factory;