feat: Allow Timelines to be configured to hide read receipts on state events.

This commit is contained in:
Doug
2025-11-26 10:08:16 +00:00
committed by Ivan Enderlin
parent 19b7036119
commit d563cebcfc
11 changed files with 151 additions and 69 deletions

View File

@@ -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();

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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,
}
}
}

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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())
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;