atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 4a96f652bdcd

tarball

atagen <boss@atagen.co>2026-10-25 08:58 UTC

3 files changed+154-34
timeline: repopulate read markers and recycle ids on a reset

The Reset arm cleared read_receipts and fully_read_event without loading them back the way an initial load does, so the read marker vanished after every history reset, and it discarded recycled_timeline_ids so every surviving row got a new unique id and re-rendered.
Modified file: crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs+59 additions−29 deletions
@@ -777,9 +777,27 @@ impl<P: RoomDataProvider> TimelineState<P> {
777777 room_data_provider: &P,
778778 receipt_type: ReceiptType,
779779 ) {
780+ self.meta
781+ .populate_initial_user_receipt(
782+ self.focus.receipt_thread(),
783+ room_data_provider,
784+ receipt_type,
785+ )
786+ .await;
787+ }
788+}
789+
790+impl TimelineMetadata {
791+ /// Populates our own latest read receipt in the in-memory by-user read
792+ /// receipt cache.
793+ pub(super) async fn populate_initial_user_receipt<P: RoomDataProvider>(
794+ &mut self,
795+ receipt_thread: ReceiptThread,
796+ room_data_provider: &P,
797+ receipt_type: ReceiptType,
798+ ) {
780799 let own_user_id = room_data_provider.own_user_id().to_owned();
781800
782- let receipt_thread = self.focus.receipt_thread();
783801 let wants_unthreaded_receipts = receipt_thread == ReceiptThread::Unthreaded;
784802
785803 let mut read_receipt = room_data_provider
@@ -794,10 +812,47 @@ impl<P: RoomDataProvider> TimelineState<P> {
794812 }
795813
796814 if let Some(read_receipt) = read_receipt {
797- self.meta.read_receipts.upsert_latest(own_user_id, receipt_type, read_receipt);
815+ self.read_receipts.upsert_latest(own_user_id, receipt_type, read_receipt);
798816 }
799817 }
800818
819+ /// Get the ID of the visible timeline event with the latest read receipt
820+ /// for the given user.
821+ pub(super) fn latest_user_read_receipt_timeline_event_id(
822+ &self,
823+ user_id: &UserId,
824+ all_remote_events: &AllRemoteEvents,
825+ ) -> Option<OwnedEventId> {
826+ // We only need to use the local map, since receipts for known events are
827+ // already loaded from the store.
828+ let public_read_receipt = self.read_receipts.get_latest(user_id, &ReceiptType::Read);
829+ let private_read_receipt =
830+ self.read_receipts.get_latest(user_id, &ReceiptType::ReadPrivate);
831+
832+ // Let's assume that a private read receipt should be more recent than a public
833+ // read receipt, otherwise there's no point in the private read receipt,
834+ // and use it as default.
835+ let (latest_receipt_id, _) = match TimelineMetadata::compare_optional_receipts(
836+ public_read_receipt,
837+ private_read_receipt,
838+ all_remote_events,
839+ ) {
840+ Ordering::Greater => public_read_receipt?,
841+ Ordering::Less => private_read_receipt?,
842+ _ => unreachable!(),
843+ };
844+
845+ // Find the corresponding visible event.
846+ all_remote_events
847+ .iter()
848+ .rev()
849+ .skip_while(|ev| ev.event_id != *latest_receipt_id)
850+ .find(|ev| ev.visible && ev.can_show_read_receipts)
851+ .map(|ev| ev.event_id.clone())
852+ }
853+}
854+
855+impl<P: RoomDataProvider> TimelineState<P> {
801856 /// Get the latest read receipt for the given user.
802857 ///
803858 /// Useful to get the latest read receipt, whether it's private or public.
@@ -851,33 +906,8 @@ impl<P: RoomDataProvider> TimelineState<P> {
851906 &self,
852907 user_id: &UserId,
853908 ) -> Option<OwnedEventId> {
854- // We only need to use the local map, since receipts for known events are
855- // already loaded from the store.
856- let public_read_receipt = self.meta.read_receipts.get_latest(user_id, &ReceiptType::Read);
857- let private_read_receipt =
858- self.meta.read_receipts.get_latest(user_id, &ReceiptType::ReadPrivate);
859-
860- // Let's assume that a private read receipt should be more recent than a public
861- // read receipt, otherwise there's no point in the private read receipt,
862- // and use it as default.
863- let (latest_receipt_id, _) = match TimelineMetadata::compare_optional_receipts(
864- public_read_receipt,
865- private_read_receipt,
866- self.items.all_remote_events(),
867- ) {
868- Ordering::Greater => public_read_receipt?,
869- Ordering::Less => private_read_receipt?,
870- _ => unreachable!(),
871- };
872-
873- // Find the corresponding visible event.
874- self.items
875- .all_remote_events()
876- .iter()
877- .rev()
878- .skip_while(|ev| ev.event_id != *latest_receipt_id)
879- .find(|ev| ev.visible && ev.can_show_read_receipts)
880- .map(|ev| ev.event_id.clone())
909+ self.meta
910+ .latest_user_read_receipt_timeline_event_id(user_id, self.items.all_remote_events())
881911 }
882912 }
883913
Modified file: crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs+36 additions−2 deletions
@@ -214,9 +214,31 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
214214 }
215215
216216 VectorDiff::Reset { values: events } => {
217- recycled_timeline_ids.clear();
217+ for event_meta in self.items.all_remote_events().iter() {
218+ if let Some(index) = event_meta.timeline_item_index {
219+ recycled_timeline_ids.insert(
220+ event_meta.event_id.clone(),
221+ self.items[index].unique_id().clone(),
222+ );
223+ }
224+ }
218225 self.clear();
226+ let track_read_receipts = settings.track_read_receipts.is_enabled();
227+ if track_read_receipts {
228+ for receipt_type in [ReceiptType::Read, ReceiptType::ReadPrivate] {
229+ self.meta
230+ .populate_initial_user_receipt(
231+ self.focus.receipt_thread(),
232+ room_data_provider,
233+ receipt_type,
234+ )
235+ .await;
236+ }
237+ }
219238 for event in events {
239+ let recycled_timeline_id = event
240+ .event_id()
241+ .and_then(|event_id| recycled_timeline_ids.remove(&event_id));
220242 self.handle_remote_event(
221243 event,
222244 TimelineItemPosition::End { origin },
@@ -224,10 +246,22 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
224246 settings,
225247 &mut date_divider_adjuster,
226248 &mut cached_profiles,
227- None,
249+ recycled_timeline_id,
228250 )
229251 .await;
230252 }
253+ if track_read_receipts {
254+ let fully_read = match room_data_provider.load_fully_read_marker().await {
255+ Some(fully_read) => Some(fully_read),
256+ None => self.meta.latest_user_read_receipt_timeline_event_id(
257+ room_data_provider.own_user_id(),
258+ self.items.all_remote_events(),
259+ ),
260+ };
261+ if let Some(fully_read) = fully_read {
262+ self.set_fully_read_event(fully_read);
263+ }
264+ }
231265 }
232266
233267 v => unimplemented!("{v:?}"),
Modified file: crates/matrix-sdk-ui/src/timeline/tests/segmented_history.rs+59 additions−3 deletions
@@ -2,14 +2,17 @@ use eyeball_im::VectorDiff;
22 use futures_util::{FutureExt, StreamExt};
33 use imbl::vector;
44 use matrix_sdk::event_cache::{HistoryGap, HistoryGapPosition, HistoryGapStatus};
5-use matrix_sdk_test::{ALICE, async_test};
5+use matrix_sdk_test::{ALICE, BOB, async_test};
66 use ruma::{
77 TransactionId, event_id,
88 events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
99 };
1010
11-use super::TestTimeline;
12-use crate::timeline::{VirtualTimelineItem, event_item::RemoteEventOrigin};
11+use super::{TestRoomDataProvider, TestTimeline, TestTimelineBuilder};
12+use crate::timeline::{
13+ TimelineReadReceiptTracking, VirtualTimelineItem, controller::TimelineSettings,
14+ event_item::RemoteEventOrigin,
15+};
1316
1417 #[async_test]
1518 async fn test_segmented_history_initial_snapshot_keeps_retained_side() {
@@ -226,3 +229,56 @@ async fn test_segmented_history_reset_preserves_pending_sends() {
226229 1
227230 );
228231 }
232+
233+#[async_test]
234+async fn test_history_reset_keeps_read_marker_and_item_identities() {
235+ let old = event_id!("$old");
236+ let new = event_id!("$new");
237+ let timeline = TestTimelineBuilder::new()
238+ .provider(TestRoomDataProvider::default().with_fully_read_marker(old.to_owned()))
239+ .settings(TimelineSettings {
240+ track_read_receipts: TimelineReadReceiptTracking::AllEvents,
241+ ..Default::default()
242+ })
243+ .build();
244+ let f = &timeline.factory;
245+ timeline
246+ .controller
247+ .replace_with_initial_remote_events(
248+ [
249+ f.text_msg("old").sender(*ALICE).event_id(old).into_event(),
250+ f.text_msg("new").sender(*BOB).event_id(new).into_event(),
251+ ],
252+ RemoteEventOrigin::Cache,
253+ )
254+ .await;
255+ let unique_id = |items: &imbl::Vector<std::sync::Arc<crate::timeline::TimelineItem>>| {
256+ items
257+ .iter()
258+ .find(|item| item.as_event().and_then(|event| event.event_id()) == Some(new))
259+ .unwrap()
260+ .unique_id()
261+ .clone()
262+ };
263+ let before = unique_id(&timeline.controller.items().await);
264+
265+ timeline
266+ .controller
267+ .handle_history_update(
268+ vec![VectorDiff::Reset {
269+ values: vector![
270+ f.text_msg("old").sender(*ALICE).event_id(old).into(),
271+ f.text_msg("new").sender(*BOB).event_id(new).into(),
272+ ],
273+ }],
274+ None,
275+ RemoteEventOrigin::Cache,
276+ )
277+ .await;
278+
279+ let items = timeline.controller.items().await;
280+ assert_eq!(unique_id(&items), before);
281+ assert!(
282+ items.iter().any(|item| matches!(item.as_virtual(), Some(VirtualTimelineItem::ReadMarker)))
283+ );
284+}