diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index da94b41e41b..be42837cdd3 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -149,6 +149,24 @@ impl RoomList { }) } + async fn invites( + &self, + listener: Box, + ) -> Result { + let (entries, entries_stream) = self.inner.invites().await.map_err(RoomListError::from)?; + + Ok(RoomListEntriesResult { + entries: entries.into_iter().map(Into::into).collect(), + entries_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move { + pin_mut!(entries_stream); + + while let Some(diff) = entries_stream.next().await { + listener.on_update(diff.into()); + } + }))), + }) + } + async fn apply_input(&self, input: RoomListInput) -> Result<(), RoomListError> { self.inner.apply_input(input.into()).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-ui/src/room_list/mod.rs b/crates/matrix-sdk-ui/src/room_list/mod.rs index 483df7fd701..d4b214c7149 100644 --- a/crates/matrix-sdk-ui/src/room_list/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list/mod.rs @@ -230,6 +230,19 @@ impl RoomList { .ok_or_else(|| Error::UnknownList(ALL_ROOMS_LIST_NAME.to_owned())) } + /// Get all previous invites, in addition to a [`Stream`] to invites. + /// + /// Invites are taking the form of `RoomListEntry`, it's like a “sub” room + /// list. + pub async fn invites( + &self, + ) -> Result<(Vector, impl Stream>), Error> { + self.sliding_sync + .on_list(INVITES_LIST_NAME, |list| ready(list.room_list_stream())) + .await + .ok_or_else(|| Error::UnknownList(INVITES_LIST_NAME.to_owned())) + } + /// Pass an [`Input`] onto the state machine. pub async fn apply_input(&self, input: Input) -> Result<(), Error> { use Input::*; diff --git a/crates/matrix-sdk-ui/src/room_list/state.rs b/crates/matrix-sdk-ui/src/room_list/state.rs index 34d9c4cc4a6..785030f9181 100644 --- a/crates/matrix-sdk-ui/src/room_list/state.rs +++ b/crates/matrix-sdk-ui/src/room_list/state.rs @@ -13,6 +13,7 @@ use super::Error; pub const ALL_ROOMS_LIST_NAME: &str = "all_rooms"; pub const VISIBLE_ROOMS_LIST_NAME: &str = "visible_rooms"; +pub const INVITES_LIST_NAME: &str = "invites"; /// The state of the [`super::RoomList`]' state machine. #[derive(Clone, Debug, PartialEq)] @@ -130,6 +131,36 @@ impl Action for SetAllRoomsListToGrowingSyncMode { } } +struct AddInvitesList; + +#[async_trait] +impl Action for AddInvitesList { + async fn run(&self, sliding_sync: &SlidingSync) -> Result<(), Error> { + sliding_sync + .add_list( + SlidingSyncList::builder(INVITES_LIST_NAME) + .sync_mode(SlidingSyncMode::new_growing(100)) + .timeline_limit(0) + .required_state(vec![ + (StateEventType::RoomAvatar, "".to_owned()), + (StateEventType::RoomEncryption, "".to_owned()), + (StateEventType::RoomMember, "$ME".to_owned()), + (StateEventType::RoomCanonicalAlias, "".to_owned()), + ]) + .filters(Some(assign!(SyncRequestListFilters::default(), { + is_invite: Some(true), + is_tombstoned: Some(false), + not_room_types: vec!["m.space".to_owned()], + + }))), + ) + .await + .map_err(Error::SlidingSync)?; + + Ok(()) + } +} + /// Type alias to represent one action. type OneAction = Box; @@ -169,7 +200,7 @@ macro_rules! actions { impl Actions { actions! { none => [], - first_rooms_are_loaded => [SetAllRoomsListToGrowingSyncMode, AddVisibleRoomsList], + first_rooms_are_loaded => [SetAllRoomsListToGrowingSyncMode, AddVisibleRoomsList, AddInvitesList], refresh_lists => [SetAllRoomsListToGrowingSyncMode], } @@ -303,4 +334,29 @@ mod tests { Ok(()) } + + #[async_test] + async fn test_action_add_invitess_list() -> Result<(), Error> { + let room_list = new_room_list().await?; + let sliding_sync = room_list.sliding_sync(); + + // List is absent. + assert_eq!(sliding_sync.on_list(INVITES_LIST_NAME, |_list| ready(())).await, None); + + // Run the action! + AddInvitesList.run(sliding_sync).await?; + + // List is present! + assert_eq!( + sliding_sync + .on_list(INVITES_LIST_NAME, |list| ready(matches!( + list.sync_mode(), + SlidingSyncMode::Growing { batch_size, .. } if batch_size == 100 + ))) + .await, + Some(true) + ); + + Ok(()) + } } diff --git a/crates/matrix-sdk-ui/tests/integration/room_list.rs b/crates/matrix-sdk-ui/tests/integration/room_list.rs index 77cacd627d0..4d7ed19138c 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list.rs @@ -8,7 +8,7 @@ use matrix_sdk_test::async_test; use matrix_sdk_ui::{ room_list::{ EntriesLoadingState, Error, Input, RoomListEntry, State, ALL_ROOMS_LIST_NAME as ALL_ROOMS, - VISIBLE_ROOMS_LIST_NAME as VISIBLE_ROOMS, + INVITES_LIST_NAME as INVITES, VISIBLE_ROOMS_LIST_NAME as VISIBLE_ROOMS, }, timeline::{TimelineItem, VirtualTimelineItem}, RoomList, @@ -326,6 +326,22 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { "sort": ["by_recency", "by_name"], "timeline_limit": 20, }, + INVITES: { + "ranges": [[0, 99]], + "required_state": [ + ["m.room.avatar", ""], + ["m.room.encryption", ""], + ["m.room.member", "$ME"], + ["m.room.canonical_alias", ""], + ], + "filters": { + "is_invite": true, + "is_tombstoned": false, + "not_room_types": ["m.space"], + }, + "sort": ["by_recency", "by_name"], + "timeline_limit": 0, + }, }, }, respond with = { @@ -341,6 +357,10 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { "count": 0, "ops": [], }, + INVITES: { + "count": 2, + "ops": [], + }, }, "rooms": { // let's ignore them for now @@ -366,6 +386,9 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { VISIBLE_ROOMS: { "ranges": [[0, 19]], }, + INVITES: { + "ranges": [[0, 1]], + } }, }, respond with = { @@ -381,6 +404,10 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { "count": 0, "ops": [], }, + INVITES: { + "count": 3, + "ops": [], + }, }, "rooms": { // let's ignore them for now @@ -401,6 +428,9 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { VISIBLE_ROOMS: { "ranges": [[0, 19]], }, + INVITES: { + "ranges": [[0, 2]], + }, }, }, respond with = { @@ -416,6 +446,10 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { "count": 0, "ops": [], }, + INVITES: { + "count": 0, + "ops": [], + } }, "rooms": { // let's ignore them for now @@ -434,7 +468,10 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { "ranges": [[0, 199]], }, VISIBLE_ROOMS: { - "ranges": [], + "ranges": [[0, 19]], + }, + INVITES: { + "ranges": [[0, 0]], }, }, }, @@ -451,6 +488,10 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { "count": 0, "ops": [], }, + INVITES: { + "count": 0, + "ops": [], + }, }, "rooms": { // let's ignore them for now @@ -466,8 +507,8 @@ async fn test_sync_from_init_to_enjoy() -> Result<(), Error> { Ok(()) } -#[async_test] +#[async_test] async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { let (server, room_list) = new_room_list().await?; @@ -515,6 +556,9 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { VISIBLE_ROOMS: { "ranges": [[0, 19]], }, + INVITES: { + "ranges": [[0, 99]], + }, }, }, respond with = { @@ -528,6 +572,10 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { "count": 0, "ops": [], }, + INVITES: { + "count": 0, + "ops": [], + }, }, "rooms": {}, }, @@ -550,6 +598,9 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { VISIBLE_ROOMS: { "ranges": [[0, 19]], }, + INVITES: { + "ranges": [[0, 0]], + }, }, }, respond with = { @@ -563,6 +614,10 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { "count": 0, "ops": [], }, + INVITES: { + "count": 0, + "ops": [], + }, }, "rooms": {}, }, @@ -644,6 +699,10 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { // Hello new list. "ranges": [[0, 19]], }, + INVITES: { + // Hello new list. + "ranges": [[0, 99]], + }, }, }, respond with = (code 400) { @@ -677,6 +736,10 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { // We have set a viewport, which reflects here. "ranges": [[5, 10]], }, + INVITES: { + // The range hasn't been modified due to previous error. + "ranges": [[0, 99]], + }, }, }, respond with = { @@ -685,6 +748,9 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { "count": 110, }, + INVITES: { + "count": 3, + } }, "rooms": {}, }, @@ -706,6 +772,10 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { // Despites the error, the range is kept. "ranges": [[5, 10]], }, + INVITES: { + // Despites the error, the range has made progress. + "ranges": [[0, 2]], + }, }, }, respond with = (code 400) { @@ -735,6 +805,10 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { // Despites the error, the range is kept. "ranges": [[5, 10]], }, + INVITES: { + // Despites the error, the range is kept. + "ranges": [[0, 2]], + } }, }, respond with = { @@ -743,6 +817,9 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { "count": 110, }, + INVITES: { + "count": 0, + }, }, "rooms": {}, }, @@ -763,6 +840,10 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { // No error. The range is still here. "ranges": [[5, 10]], }, + INVITES: { + // The range is making progress. + "ranges": [[0, 0]], + }, }, }, respond with = { @@ -792,6 +873,10 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { // The range is still here. "ranges": [[5, 10]], }, + INVITES: { + // The range is kept as it was. + "ranges": [[0, 0]], + }, }, }, respond with = (code 400) { @@ -823,6 +908,10 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { // The range is still here. "ranges": [[5, 10]], }, + INVITES: { + // The range is kept as it was. + "ranges": [[0, 0]], + }, }, }, respond with = { @@ -905,7 +994,7 @@ async fn test_entries_stream() -> Result<(), Error> { set[1] [ F("!r1:bar.org") ]; set[2] [ F("!r2:bar.org") ]; pending; - } + }; sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -913,13 +1002,14 @@ async fn test_entries_stream() -> Result<(), Error> { assert request = { "lists": { ALL_ROOMS: { - "ranges": [ - [0, 9], - ], + "ranges": [[0, 9]], }, VISIBLE_ROOMS: { "ranges": [[0, 19]], }, + INVITES: { + "ranges": [[0, 99]], + }, }, }, respond with = { @@ -939,15 +1029,15 @@ async fn test_entries_stream() -> Result<(), Error> { { "op": "INSERT", "index": 0, - "room_id": "!r3:bar.org" + "room_id": "!r3:bar.org", }, ], }, VISIBLE_ROOMS: { "count": 0, - "ops": [ - // let's ignore them for now - ], + }, + INVITES: { + "count": 0, }, }, "rooms": { @@ -966,7 +1056,7 @@ async fn test_entries_stream() -> Result<(), Error> { remove[0]; insert[0] [ F("!r3:bar.org") ]; pending; - } + }; Ok(()) } @@ -1046,6 +1136,9 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { VISIBLE_ROOMS: { "ranges": [[0, 19]], }, + INVITES: { + "ranges": [[0, 99]], + }, }, }, respond with = { @@ -1068,9 +1161,9 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { }, VISIBLE_ROOMS: { "count": 0, - "ops": [ - // let's ignore them for now - ], + }, + INVITES: { + "count": 0, }, }, "rooms": { @@ -1109,6 +1202,162 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { Ok(()) } +#[async_test] +async fn test_invites_stream() -> Result<(), Error> { + let (server, room_list) = new_room_list().await?; + + let sync = room_list.sync(); + pin_mut!(sync); + + // The invites aren't accessible yet. + assert!(room_list.invites().await.is_err()); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + states = Init => FirstRooms, + assert request = { + "lists": { + ALL_ROOMS: { + "ranges": [[0, 19]], + }, + }, + }, + respond with = { + "pos": "0", + "lists": { + ALL_ROOMS: { + "count": 0, + }, + }, + "rooms": {}, + }, + }; + + // The invites aren't accessible yet. + assert!(room_list.invites().await.is_err()); + + let room_id_0 = room_id!("!r0:bar.org"); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + states = FirstRooms => AllRooms, + assert request = { + "lists": { + ALL_ROOMS: { + "ranges": [[0, 0]], + }, + VISIBLE_ROOMS: { + "ranges": [[0, 19]], + }, + INVITES: { + "ranges": [[0, 99]], + }, + }, + }, + respond with = { + "pos": "1", + "lists": { + ALL_ROOMS: { + "count": 0, + }, + VISIBLE_ROOMS: { + "count": 0, + }, + INVITES: { + "count": 1, + "ops": [ + { + "op": "SYNC", + "range": [0, 0], + "room_ids": [ + room_id_0, + ], + }, + ], + }, + }, + "rooms": { + room_id_0: { + "name": "Invitation for Room #0", + "initial": true, + }, + }, + }, + }; + + let (previous_invites, invites_stream) = room_list.invites().await?; + pin_mut!(invites_stream); + + assert_eq!(previous_invites.len(), 1); + assert_matches!(&previous_invites[0], RoomListEntry::Filled(room_id) => { + assert_eq!(room_id, room_id_0); + }); + + assert_entries_stream! { + [invites_stream] + pending; + }; + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + states = AllRooms => CarryOn, + assert request = { + "lists": { + ALL_ROOMS: { + "ranges": [[0, 0]], + }, + VISIBLE_ROOMS: { + "ranges": [[0, 19]], + }, + INVITES: { + "ranges": [[0, 0]], + }, + }, + }, + respond with = { + "pos": "2", + "lists": { + ALL_ROOMS: { + "count": 0, + }, + VISIBLE_ROOMS: { + "count": 0, + }, + INVITES: { + "count": 1, + "ops": [ + { + "op": "DELETE", + "index": 0, + }, + { + + "op": "INSERT", + "index": 0, + "room_id": "!r1:bar.org", + }, + ], + }, + }, + "rooms": { + "!r1:bar.org": { + "name": "Invitation for Room #1", + "initial": true, + }, + }, + }, + }; + + assert_entries_stream! { + [invites_stream] + remove[0]; + insert[0] [ F("!r1:bar.org") ]; + pending; + }; + + Ok(()) +} + #[async_test] async fn test_room() -> Result<(), Error> { let (server, room_list) = new_room_list().await?; @@ -1634,6 +1883,9 @@ async fn test_input_viewport() -> Result<(), Error> { "ranges": [[0, 19]], "timeline_limit": 20, }, + INVITES: { + "ranges": [[0, 99]], + }, }, }, respond with = { @@ -1657,6 +1909,9 @@ async fn test_input_viewport() -> Result<(), Error> { "ranges": [[10, 15], [20, 25]], "timeline_limit": 20, }, + INVITES: { + "ranges": [[0, 99]], + }, }, }, respond with = {