-
Notifications
You must be signed in to change notification settings - Fork 362
Description
The issue consists of multiple problems:
- Focused timelines fetch from the homeserver instead of the event cache
- Focused timelines never populate the event cache with events
- The event cache sends out updates using indices
As of 2025.12.11. we have the following focused timelines:
TimelineFocus::Live- The normal timeline, showing all events.TimelineFocus::Event- A timeline focusing on a specific event and a number of contextual events around it.TimelineFocus::Thread- A timeline focusing on a specific thread.TimelineFocus::PinnedEvents- A timeline showing only the pinned events of a room.
The latter three are what are considered to be focused timelines and work differently from the normal Live timeline. The event cache works on all events in a specific order, while focused timelines work on a relatively small subset of events.
The event cache gets populated by the sync and by backpagination requests of the live timeline. This makes it possible to know where to insert a new chunk of events and each event gets assigned a certain order and index.
These indices are used by the event cache to notify listeners, i.e. the live timeline, to update its view.
So if a new event comes around or an event replaces another in the event cache, you might get something like "Set this event at index 0" or "Replace event at index 7 with this event".
Now a focused timeline only shows a subset of all events and is initially populated by getting events from the homeserver. The events are never put into the event cache and thus the event cache never assigned an index to those events.
Meaning, the focused timelines can't really react to the event cache providing live updates.
This issue is briefly mentioned in #5053 (comment)
I'll copy the relevant snippet over:
For the other timelines, though, it's a lie: we're still accumulating metadata from the events loaded in the context of those timelines, and putting them into all_remote_events, but they're not modeled after the live updates from the event cache. As a result, we can't apply the live updates in real-time (because the indices from the event cache updates won't match any of the events backing the non-live timeline), and have to complicate the implementation of handling live events related to a non-live timeline (see last commit in #5060).
This has been the reason of at least the following workarounds and issues:
- feat(timeline): handle live aggregations on non-live timelines #5060
- fix(timeline): Don't add events to the pinned events timeline on paginations/syncs #4645
- thread summary: UTD not updated after being correctly decrypted #5703
- UTDs on pinned messages are not retried after key is received #5798
Another set of issues stems from the fact that the redecryptor (R2D2) operates on events in the event cache. Since focused timelines don't populate the event cache, R2D2 won't necessarily be able to redecrypt events that are cached in focused timelines.
The following integration tests showcase this failure for two of the focused timelines:
matrix-rust-sdk/testing/matrix-sdk-integration-testing/src/tests/timeline.rs
Lines 1258 to 1269 in b3f6df9
| /// Test that pinned UTD events, once decrypted by R2D2 (the redecryptor), get | |
| /// replaced in the timeline with the decrypted variant even if the pinened UTD | |
| /// event isn't part of the main timeline and thus wasn't put into the event | |
| /// cache by the main timeline backpaginating. | |
| #[tokio::test(flavor = "multi_thread", worker_threads = 4)] | |
| // FIXME: This test is ignored because R2D2 can't decrypt this pinned event as it's never put into | |
| // the event cache. | |
| #[ignore] | |
| async fn test_pinned_events_are_decrypted_after_recovering_with_event_not_in_timeline() -> TestResult | |
| { | |
| test_pinned_events_are_decrypted_after_recovering_with_event_count(30).await | |
| } |
matrix-rust-sdk/testing/matrix-sdk-integration-testing/src/tests/timeline.rs
Lines 1271 to 1388 in b3f6df9
| /// Test that UTDs in a timeline focused on a single event, once decrypted by | |
| /// R2D2 (the redecryptor), get replaced in the timeline with the decrypted | |
| /// variant even if the focused UTD event isn't part of the main timeline and | |
| /// thus wasn't put into the event cache by the main timeline backpaginating. | |
| #[tokio::test(flavor = "multi_thread", worker_threads = 4)] | |
| // FIXME: This test is ignored because R2D2 can't decrypt this event as | |
| // it's never put into the event cache. | |
| #[ignore] | |
| async fn test_permalink_timelines_redecrypt() -> TestResult { | |
| const RECOVERY_PASSPHRASE: &str = "I am error"; | |
| let encryption_settings = EncryptionSettings { | |
| auto_enable_cross_signing: true, | |
| auto_enable_backups: true, | |
| backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure, | |
| }; | |
| // Set up sync for user Alice, and create a room. | |
| let alice = TestClientBuilder::new("alice") | |
| .encryption_settings(encryption_settings) | |
| .use_sqlite() | |
| .build() | |
| .await?; | |
| let user_id = alice.user_id().expect("We should have a user ID by now"); | |
| // We can reuse the function for pinned events and the pinned event as our | |
| // targeted event. | |
| let (room_id, event_id) = | |
| prepare_room_with_pinned_events(&alice, RECOVERY_PASSPHRASE, 30).await?; | |
| // Now `another_alice` comes into play. | |
| let another_alice = TestClientBuilder::with_exact_username(user_id.localpart().to_owned()) | |
| .encryption_settings(encryption_settings) | |
| .use_sqlite() | |
| .build() | |
| .await?; | |
| // Alright, we're done with the original Alice. | |
| drop(alice); | |
| // No rooms as of yet, we have not synced with the server as of yet. | |
| assert!(another_alice.rooms().is_empty()); | |
| another_alice.event_cache().subscribe()?; | |
| let sync_service = SyncService::builder(another_alice.clone()).build().await?; | |
| // We need to subscribe to the room, otherwise we won't request the | |
| // `m.room.pinned_events` state event. | |
| // | |
| // Additionally if we subscribe to the room after we already synced, we won't | |
| // receive the event, likely due to a Synapse bug. | |
| sync_service.room_list_service().subscribe_to_rooms(&[&room_id]).await; | |
| sync_service.start().await; | |
| another_alice.encryption().wait_for_e2ee_initialization_tasks().await; | |
| // Let's get the room. | |
| let room = wait_for_room(&another_alice, &room_id).await; | |
| assert!(room.latest_encryption_state().await?.is_encrypted(), "The room should be encrypted"); | |
| // Let's see if the event is there, it should be a UTD. | |
| let event = room.event(&event_id, Default::default()).await?; | |
| assert!(event.kind.is_utd()); | |
| // Alright, let's now go to the timeline with an Event focus. | |
| let permalink_timeline = room | |
| .timeline_builder() | |
| .with_focus(TimelineFocus::Event { | |
| target: event_id.to_owned(), | |
| num_context_events: 1, | |
| hide_threaded_events: true, | |
| }) | |
| .build() | |
| .await?; | |
| let (items, mut stream) = | |
| permalink_timeline.subscribe_filter_map(|i| i.as_event().cloned()).await; | |
| // If we don't have any items as of yet, wait on the stream. | |
| if items.is_empty() { | |
| let _ = assert_next_with_timeout!(stream, 5000); | |
| } | |
| // Alright, let's get the event from the timeline. | |
| let item = permalink_timeline | |
| .item_by_event_id(&event_id) | |
| .await | |
| .expect("We should have access to the focused event"); | |
| // Still a UTD. | |
| assert!( | |
| item.content().is_unable_to_decrypt(), | |
| "The focused event should be a UTD as we didn't recover yet" | |
| ); | |
| assert_eq!(permalink_timeline.items().await.len(), 3); | |
| // Let's now recover. | |
| another_alice.encryption().recovery().recover(RECOVERY_PASSPHRASE).await?; | |
| assert_eq!(another_alice.encryption().recovery().state(), RecoveryState::Enabled); | |
| // The next update for the timeline should replace the UTD item with a decrypted | |
| // value. | |
| let next_item = assert_next_with_timeout!(stream, 5000); | |
| assert_let!(VectorDiff::Set { index: 0, value } = next_item); | |
| let content = value.content(); | |
| // And we're not a UTD anymore. | |
| assert!(!content.is_unable_to_decrypt()); | |
| let message = content.as_message().expect("The focused event should be a message"); | |
| assert_eq!(message.body(), "It's a secret to everybody"); | |
| // And we check that we don't have any more items in the timeline, the UTD item | |
| // was indeed replaced. | |
| let items = permalink_timeline.items().await; | |
| assert_eq!(items.len(), 3); | |
| Ok(()) | |
| } |