From c1a24cf0337b8459dd3e3d4e8b35f6777bc42208 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 2 Jun 2023 22:32:57 +0200 Subject: [PATCH] feat(ui): Implement `Room::timeline` and `::latest_event`. This patch changes `RoomInner` to have an `room: Option` field to `room: matrix_sdk::room::Room`. `room` is not longer an `Option`. Then, it's easier to have a new `timeline: Timeline` field, along with a `sneaky_timeline: Timeline` field. Both are used by the new `Room::timeline()` and `Room::latest_event()` method. --- crates/matrix-sdk-ui/src/room_list/mod.rs | 61 +++++- .../tests/integration/room_list.rs | 183 +++++++++++++++++- .../tests/integration/timeline/mod.rs | 2 +- .../integration/timeline/sliding_sync.rs | 11 +- 4 files changed, 243 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list/mod.rs b/crates/matrix-sdk-ui/src/room_list/mod.rs index 387650077..e1305dbee 100644 --- a/crates/matrix-sdk-ui/src/room_list/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list/mod.rs @@ -77,6 +77,8 @@ use once_cell::sync::Lazy; use ruma::{OwnedRoomId, RoomId}; use thiserror::Error; +use crate::{timeline::EventTimelineItem, Timeline}; + pub const ALL_ROOMS_LIST_NAME: &str = "all_rooms"; pub const VISIBLE_ROOMS_LIST_NAME: &str = "visible_rooms"; @@ -222,12 +224,12 @@ impl RoomList { Ok(()) } + /// Get a [`Room`] if it exists. pub async fn room(&self, room_id: &RoomId) -> Result { - self.sliding_sync - .get_room(room_id) - .await - .map(Room::new) - .ok_or_else(|| Error::RoomNotFound(room_id.to_owned())) + match self.sliding_sync.get_room(room_id).await { + Some(room) => Room::new(room).await, + None => Err(Error::RoomNotFound(room_id.to_owned())), + } } #[cfg(any(test, feature = "testing"))] @@ -246,16 +248,42 @@ pub struct Room { #[derive(Debug)] struct RoomInner { + /// The Sliding Sync room. sliding_sync_room: SlidingSyncRoom, - room: Option, + + /// The underlying client room. + room: matrix_sdk::room::Room, + + /// The timeline of the room. + timeline: Timeline, + + /// The “sneaky” timeline of the room, i.e. this timeline doesn't track the + /// read marker nor the receipts. + sneaky_timeline: Timeline, } impl Room { /// Create a new `Room`. - fn new(sliding_sync_room: SlidingSyncRoom) -> Self { - let room = sliding_sync_room.client().get_room(sliding_sync_room.room_id()); + async fn new(sliding_sync_room: SlidingSyncRoom) -> Result { + let room = sliding_sync_room + .client() + .get_room(sliding_sync_room.room_id()) + .ok_or_else(|| Error::RoomNotFound(sliding_sync_room.room_id().to_owned()))?; - Self { inner: Arc::new(RoomInner { sliding_sync_room, room }) } + let timeline = Timeline::builder(&room) + .events(sliding_sync_room.prev_batch(), sliding_sync_room.timeline_queue()) + .track_read_marker_and_receipts() + .build() + .await; + + let sneaky_timeline = Timeline::builder(&room) + .events(sliding_sync_room.prev_batch(), sliding_sync_room.timeline_queue()) + .build() + .await; + + Ok(Self { + inner: Arc::new(RoomInner { sliding_sync_room, room, timeline, sneaky_timeline }), + }) } /// Get the best possible name for the room. @@ -265,9 +293,22 @@ impl Room { pub async fn name(&self) -> Option { Some(match self.inner.sliding_sync_room.name() { Some(name) => name, - None => self.inner.room.as_ref()?.display_name().await.ok()?.to_string(), + None => self.inner.room.display_name().await.ok()?.to_string(), }) } + + /// Get the timeline of the room. + pub fn timeline(&self) -> &Timeline { + &self.inner.timeline + } + + /// Get the latest event of the timeline. + /// + /// It's different from `Self::timeline().latest_event()` as it won't track + /// the read marker and receipts. + pub async fn latest_event(&self) -> Option { + self.inner.sneaky_timeline.latest_event().await + } } /// [`RoomList`]'s errors. diff --git a/crates/matrix-sdk-ui/tests/integration/room_list.rs b/crates/matrix-sdk-ui/tests/integration/room_list.rs index c8f7a570c..8e94d1c02 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list.rs @@ -8,13 +8,17 @@ use matrix_sdk_ui::{ Error, Input, RoomListEntry, State, ALL_ROOMS_LIST_NAME as ALL_ROOMS, VISIBLE_ROOMS_LIST_NAME as VISIBLE_ROOMS, }, + timeline::{TimelineItem, VirtualTimelineItem}, RoomList, }; -use ruma::room_id; +use ruma::{event_id, room_id}; use serde_json::json; use wiremock::{http::Method, Match, Mock, MockServer, Request, ResponseTemplate}; -use crate::logged_in_client; +use crate::{ + logged_in_client, + timeline::sliding_sync::{assert_timeline_stream, timeline_event}, +}; async fn new_room_list() -> Result<(MockServer, RoomList), Error> { let (client, server) = logged_in_client().await; @@ -827,6 +831,181 @@ async fn test_room_not_found() -> Result<(), Error> { Ok(()) } +#[async_test] +async fn test_room_timeline() -> Result<(), Error> { + let (server, room_list) = new_room_list().await?; + + let sync = room_list.sync(); + pin_mut!(sync); + + let room_id = room_id!("!r0:bar.org"); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + assert request = {}, + respond with = { + "pos": "0", + "lists": { + ALL_ROOMS: { + "count": 2, + "ops": [ + { + "op": "SYNC", + "range": [0, 0], + "room_ids": [room_id], + }, + ], + }, + }, + "rooms": { + room_id: { + "name": "Room #0", + "initial": true, + "timeline": [ + timeline_event!("$x0:bar.org" at 0 sec), + ], + }, + }, + }, + }; + + let room = room_list.room(room_id).await?; + let timeline = room.timeline(); + + let (previous_timeline_items, mut timeline_items_stream) = timeline.subscribe().await; + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + assert request = {}, + respond with = { + "pos": "0", + "lists": {}, + "rooms": { + room_id: { + "timeline": [ + timeline_event!("$x1:bar.org" at 1 sec), + timeline_event!("$x2:bar.org" at 2 sec), + ], + }, + }, + }, + }; + + // Previous timeline items. + assert_matches!( + previous_timeline_items[0].as_ref(), + TimelineItem::Virtual(VirtualTimelineItem::DayDivider(_)) + ); + assert_matches!( + previous_timeline_items[1].as_ref(), + TimelineItem::Event(item) => { + assert_eq!(item.event_id().unwrap().as_str(), "$x0:bar.org"); + } + ); + + // Timeline items stream. + assert_timeline_stream! { + [timeline_items_stream] + update[1] "$x0:bar.org"; + append "$x1:bar.org"; + update[2] "$x1:bar.org"; + append "$x2:bar.org"; + }; + + Ok(()) +} + +#[async_test] +async fn test_room_latest_event() -> Result<(), Error> { + let (server, room_list) = new_room_list().await?; + + let sync = room_list.sync(); + pin_mut!(sync); + + let room_id = room_id!("!r0:bar.org"); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + assert request = {}, + respond with = { + "pos": "0", + "lists": { + ALL_ROOMS: { + "count": 2, + "ops": [ + { + "op": "SYNC", + "range": [0, 0], + "room_ids": [room_id], + }, + ], + }, + }, + "rooms": { + room_id: { + "name": "Room #0", + "initial": true, + }, + }, + }, + }; + + let room = room_list.room(room_id).await?; + + // The latest event does not exist. + assert!(room.latest_event().await.is_none()); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + assert request = {}, + respond with = { + "pos": "0", + "lists": {}, + "rooms": { + room_id: { + "timeline": [ + timeline_event!("$x0:bar.org" at 0 sec), + ], + }, + }, + }, + }; + + // The latest event exists. + assert_matches!( + room.latest_event().await, + Some(event) => { + assert_eq!(event.event_id(), Some(event_id!("$x0:bar.org"))); + } + ); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + assert request = {}, + respond with = { + "pos": "0", + "lists": {}, + "rooms": { + room_id: { + "timeline": [ + timeline_event!("$x1:bar.org" at 1 sec), + ], + }, + }, + }, + }; + + // The latest event has been updated. + assert_matches!( + room.latest_event().await, + Some(event) => { + assert_eq!(event.event_id(), Some(event_id!("$x1:bar.org"))); + } + ); + + Ok(()) +} + #[async_test] async fn test_input_viewport() -> Result<(), Error> { let (server, room_list) = new_room_list().await?; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index a5b1588ed..a4870f542 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -28,7 +28,7 @@ use wiremock::{ mod read_receipts; #[cfg(feature = "experimental-sliding-sync")] -mod sliding_sync; +pub(crate) mod sliding_sync; use crate::{logged_in_client, mock_encryption_state, mock_sync}; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs index d8a006949..a6b7cd3dc 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs @@ -50,6 +50,8 @@ macro_rules! timeline_event { } } +pub(crate) use timeline_event; + macro_rules! assert_timeline_stream { // `--- day divider ---` ( @_ [ $stream:ident ] [ --- day divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { @@ -63,7 +65,12 @@ macro_rules! assert_timeline_stream { assert_matches!( $stream.next().now_or_never(), Some(Some(VectorDiff::PushBack { value })) => { - assert_matches!(value.as_ref(), TimelineItem::Virtual(VirtualTimelineItem::DayDivider(_))); + assert_matches!( + value.as_ref(), + TimelineItem::Virtual( + VirtualTimelineItem::DayDivider(_) + ) + ); } ); } @@ -148,6 +155,8 @@ macro_rules! assert_timeline_stream { }; } +pub(crate) use assert_timeline_stream; + async fn new_sliding_sync(lists: Vec) -> Result<(MockServer, SlidingSync)> { let (client, server) = logged_in_client().await;