diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs index 9da6bb80a59..83b3c40ed40 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs @@ -753,3 +753,144 @@ async fn test_focused_timeline_handles_other_thread_event_when_forcing_threaded_ assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); assert_eq!(item.as_event().unwrap().content().as_message().unwrap().body(), "Next"); } + +#[async_test] +async fn test_focused_timeline_filters_out_threaded_events() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let user_id = client.user_id().unwrap(); + + let prev_thread_event_id = event_id!("$prev:example.org"); + let root_thread_id = event_id!("$root-id:example.org"); + + let f = EventFactory::new().room(room_id).sender(user_id); + let focus_event = f.text_msg("Hey").into_event(); + let focus_event_id = focus_event.event_id().unwrap().clone(); + + // Mock the initial /context request to check if the event is in a thread. + let events_before = vec![ + f.text_msg("Unrelated before 1").into_event(), + f.text_msg("Unrelated before 2").into_event(), + f.text_msg("Thread before").in_thread(root_thread_id, prev_thread_event_id).into_event(), + ]; + let events_after = vec![ + f.text_msg("Unrelated after 1").into_event(), + f.text_msg("Unrelated after 2").into_event(), + f.text_msg("Thread after").in_thread(root_thread_id, prev_thread_event_id).into_event(), + ]; + server + .mock_room_event_context() + .room(room_id) + .ok(RoomContextResponseTemplate::new(focus_event) + .events_before(events_before) + .events_after(events_after) + .start("prev_token_1") + .end("next_token_1")) + .mock_once() + .mount() + .await; + + let focus = TimelineFocus::Event { + target: focus_event_id, + num_context_events: 10, + thread_mode: TimelineEventFocusThreadMode::Automatic { hide_threaded_events: true }, + }; + + let room = server.sync_joined_room(&client, room_id).await; + let timeline = TimelineBuilder::new(&room) + .with_focus(focus) + .build() + .await + .expect("Could not build focused timeline"); + + assert!( + timeline.live_back_pagination_status().await.is_none(), + "there should be no live back-pagination status for a focused timeline" + ); + + let (items, mut timeline_stream) = timeline.subscribe().await; + // The 2 unrelated events before + 2 after + the item + the date divider + assert_eq!(items.len(), 6); + assert!(items[0].is_date_divider()); + assert_eq!( + items[1].as_event().unwrap().content().as_message().unwrap().body(), + "Unrelated before 2" + ); + assert_eq!( + items[2].as_event().unwrap().content().as_message().unwrap().body(), + "Unrelated before 1" + ); + assert_eq!(items[3].as_event().unwrap().content().as_message().unwrap().body(), "Hey"); + assert_eq!( + items[4].as_event().unwrap().content().as_message().unwrap().body(), + "Unrelated after 1" + ); + assert_eq!( + items[5].as_event().unwrap().content().as_message().unwrap().body(), + "Unrelated after 2" + ); + + // We paginate back once + server + .mock_room_messages() + .match_from("prev_token_1") + .ok(RoomMessagesResponseTemplate { + chunk: vec![ + f.text_msg("Prev") + .sender(user_id) + .in_thread(root_thread_id, prev_thread_event_id) + .into_raw_timeline(), + f.text_msg("Prev no thread").sender(user_id).into_raw_timeline(), + ], + start: "prev_token_1".to_owned(), + end: Some("prev_token_2".to_owned()), + state: Vec::new(), + delay: None, + }) + .mock_once() + .mount() + .await; + + let start_of_timeline = + timeline.paginate_backwards(10).await.expect("Could not paginate backwards"); + assert!(!start_of_timeline); + + // Only the non-threaded event is inserted at the start. + assert_let_timeout!(Some(timeline_updates) = timeline_stream.next()); + assert_eq!(timeline_updates.len(), 1); + // The new item loaded is inserted at the start, just after the date divider. + assert_let!(VectorDiff::Insert { index: 1, value: item } = &timeline_updates[0]); + assert_eq!(item.as_event().unwrap().content().as_message().unwrap().body(), "Prev no thread"); + + // Then we paginate forwards + server + .mock_room_messages() + .match_from("next_token_1") + .ok(RoomMessagesResponseTemplate { + chunk: vec![ + f.text_msg("Next no thread").sender(user_id).into_raw_timeline(), + f.text_msg("Next1") + .sender(user_id) + .in_thread(root_thread_id, prev_thread_event_id) + .into_raw_timeline(), + ], + start: "next_token_1".to_owned(), + end: Some("next_token_2".to_owned()), + state: Vec::new(), + delay: None, + }) + .mock_once() + .mount() + .await; + + let end_of_timeline = + timeline.paginate_forwards(10).await.expect("Could not paginate forwards"); + assert!(!end_of_timeline); + + // Only the non-threaded event is pushed to the end. + assert_let_timeout!(Some(timeline_updates) = timeline_stream.next()); + assert_eq!(timeline_updates.len(), 1); + assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); + assert_eq!(item.as_event().unwrap().content().as_message().unwrap().body(), "Next no thread"); +} diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 2f0df376029..1571558f1aa 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -140,6 +140,9 @@ All notable changes to this project will be documented in this file. ### Bugfix +- When threads are enabled, a focused event timeline is used and the focused event is not part of a thread, + hide other threaded events by default like it happens on the live focus timeline. + ([#6519](https://github.com/matrix-org/matrix-rust-sdk/pull/6519)) - Add a recursion limit attribute that raises it from the default value of 128 to 256. ([#6489](https://github.com/matrix-org/matrix-rust-sdk/pull/6489)) - Reject invalid edits as candidates for the latest event. diff --git a/crates/matrix-sdk/src/event_cache/caches/event_focused/mod.rs b/crates/matrix-sdk/src/event_cache/caches/event_focused/mod.rs index ff876519c6a..800011a6e38 100644 --- a/crates/matrix-sdk/src/event_cache/caches/event_focused/mod.rs +++ b/crates/matrix-sdk/src/event_cache/caches/event_focused/mod.rs @@ -81,7 +81,7 @@ pub enum EventFocusThreadMode { pub(crate) enum EventFocusedPaginationMode { /// Standard room pagination (for all events as for an unthreaded/main room /// linked chunk). - Room, + Room { hide_thread_events: bool }, /// Threaded pagination (the focused event is part of a thread). Thread { @@ -210,12 +210,27 @@ impl EventFocusedCacheInner { self.add_initial_events_with_gaps(thread_events, backward_token, forward_token); } else { trace!("focused event is not part of a thread, setting up room pagination"); - self.pagination_mode = EventFocusedPaginationMode::Room; let backward_token = tokens.previous.into_token(); let forward_token = tokens.next.into_token(); - self.add_initial_events_with_gaps(result.events.clone(), backward_token, forward_token); + let hide_thread_events = + matches!(thread_mode, EventFocusThreadMode::Automatic) && thread_root.is_none(); + + self.pagination_mode = EventFocusedPaginationMode::Room { hide_thread_events }; + + let events = if hide_thread_events { + result + .events + .iter() + .filter(|event| extract_thread_root(event.raw()).is_none()) + .cloned() + .collect() + } else { + result.events.clone() + }; + + self.add_initial_events_with_gaps(events, backward_token, forward_token); } self.propagate_changes(); @@ -304,7 +319,7 @@ impl EventFocusedCacheInner { // Fetch events based on pagination mode. let (mut events, new_token) = match &self.pagination_mode { - EventFocusedPaginationMode::Room => { + EventFocusedPaginationMode::Room { .. } => { Self::fetch_room_backwards(&room, num_events, &token).await? } EventFocusedPaginationMode::Thread { thread_root } => { @@ -319,6 +334,17 @@ impl EventFocusedCacheInner { let hit_end = new_token.is_none(); let new_gap = new_token.map(|t| Gap { token: t }); + let hide_thread_events = match &self.pagination_mode { + EventFocusedPaginationMode::Room { hide_thread_events } => *hide_thread_events, + EventFocusedPaginationMode::Thread { .. } => false, + }; + + let events = if hide_thread_events { + events.into_iter().filter(|event| extract_thread_root(event.raw()).is_none()).collect() + } else { + events + }; + // Replace the gap and insert the new events. self.chunk.push_backwards_pagination_events(Some(gap_id), new_gap, &events); @@ -405,7 +431,7 @@ impl EventFocusedCacheInner { // Fetch events based on pagination mode. let (events, new_token) = match &self.pagination_mode { - EventFocusedPaginationMode::Room => { + EventFocusedPaginationMode::Room { .. } => { Self::fetch_room_forwards(&room, num_events, &token).await? } EventFocusedPaginationMode::Thread { thread_root } => { @@ -416,6 +442,17 @@ impl EventFocusedCacheInner { let hit_end = new_token.is_none(); let new_gap = new_token.map(|t| Gap { token: t }); + let hide_thread_events = match &self.pagination_mode { + EventFocusedPaginationMode::Room { hide_thread_events } => *hide_thread_events, + EventFocusedPaginationMode::Thread { .. } => false, + }; + + let events = if hide_thread_events { + events.into_iter().filter(|event| extract_thread_root(event.raw()).is_none()).collect() + } else { + events + }; + // Replace the gap and insert new events. self.chunk.push_forwards_pagination_events(Some(gap_id), new_gap, &events); @@ -497,7 +534,7 @@ impl EventFocusedCache { inner: Arc::new(RwLock::new(EventFocusedCacheInner { room, focused_event_id, - pagination_mode: EventFocusedPaginationMode::Room, + pagination_mode: EventFocusedPaginationMode::Room { hide_thread_events: false }, chunk: EventLinkedChunk::new(), sender: Sender::new(32), linked_chunk_update_sender,