mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-07 23:44:53 -04:00
feat: Allow Timelines to be configured to hide read receipts on state events.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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<OwnedEventId>,
|
||||
) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TimelineEventFilterFn>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<P: RoomDataProvider> 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<P: RoomDataProvider> TimelineState<P> {
|
||||
.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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TimelineAction>,
|
||||
Option<OwnedEventId>,
|
||||
bool,
|
||||
bool,
|
||||
)> {
|
||||
let state_key: Option<String> = 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user