mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-19 06:04:31 -04:00
feat(spaces): introduce a SpaceRoomList that allows pagination and provides reactive interfaces to its rooms and pagination state
This commit is contained in:
committed by
Stefan Ceriu
parent
a43e42c170
commit
b8be1fdb26
@@ -12,8 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
room_list::SpaceServiceRoomListPaginationState as UISpaceServiceRoomListPaginationState,
|
||||
SpaceService as UISpaceService, SpaceServiceRoom as UISpaceServiceRoom,
|
||||
SpaceServiceRoomList as UISpaceServiceRoomList,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
@@ -22,6 +28,8 @@ use crate::{
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_preview::RoomType,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -30,7 +38,6 @@ pub struct SpaceService {
|
||||
}
|
||||
|
||||
impl SpaceService {
|
||||
/// Create a new instance of `SpaceService`.
|
||||
pub fn new(inner: UISpaceService) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
@@ -38,22 +45,99 @@ impl SpaceService {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceService {
|
||||
/// Get the list of joined spaces.
|
||||
pub fn joined_spaces(&self) -> Vec<SpaceServiceRoom> {
|
||||
self.inner.joined_spaces().into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Get the top-level children for a given space.
|
||||
pub async fn top_level_children_for(
|
||||
pub fn top_level_children_for(
|
||||
&self,
|
||||
space_id: String,
|
||||
) -> Result<Vec<SpaceServiceRoom>, ClientError> {
|
||||
) -> Result<SpaceServiceRoomList, ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
let children = self.inner.top_level_children_for(space_id).await?;
|
||||
Ok(children.into_iter().map(Into::into).collect())
|
||||
Ok(SpaceServiceRoomList::new(self.inner.space_room_list(space_id)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SpaceServiceRoomList {
|
||||
inner: UISpaceServiceRoomList,
|
||||
}
|
||||
|
||||
impl SpaceServiceRoomList {
|
||||
pub fn new(inner: UISpaceServiceRoomList) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceServiceRoomList {
|
||||
pub fn pagination_state(&self) -> SpaceServiceRoomListPaginationState {
|
||||
self.inner.pagination_state().into()
|
||||
}
|
||||
|
||||
pub fn subscribe_to_pagination_state_updates(
|
||||
&self,
|
||||
listener: Box<dyn SpaceServiceRoomListPaginationStateListener>,
|
||||
) {
|
||||
let pagination_state = self.inner.subscribe_to_pagination_state_updates();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(pagination_state);
|
||||
|
||||
while let Some(state) = pagination_state.next().await {
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> Vec<SpaceServiceRoom> {
|
||||
self.inner.rooms().into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
pub fn subscribe_to_room_update(&self, listener: Box<dyn SpaceServiceRoomListEntriesListener>) {
|
||||
let entries_stream = self.inner.subscribe_to_room_updates();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
while let Some(rooms) = entries_stream.next().await {
|
||||
listener.on_update(rooms.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SpaceServiceRoomListPaginationState {
|
||||
Idle { end_reached: bool },
|
||||
Loading,
|
||||
}
|
||||
|
||||
impl From<UISpaceServiceRoomListPaginationState> for SpaceServiceRoomListPaginationState {
|
||||
fn from(state: UISpaceServiceRoomListPaginationState) -> Self {
|
||||
match state {
|
||||
UISpaceServiceRoomListPaginationState::Idle { end_reached } => {
|
||||
SpaceServiceRoomListPaginationState::Idle { end_reached }
|
||||
}
|
||||
UISpaceServiceRoomListPaginationState::Loading => {
|
||||
SpaceServiceRoomListPaginationState::Loading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceServiceRoomListPaginationStateListener:
|
||||
SendOutsideWasm + SyncOutsideWasm + Debug
|
||||
{
|
||||
fn on_update(&self, pagination_state: SpaceServiceRoomListPaginationState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceServiceRoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, rooms: Vec<SpaceServiceRoom>);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SpaceServiceRoom {
|
||||
pub room_id: String,
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
|
||||
//! High level interfaces for working with Spaces
|
||||
//!
|
||||
//! See [`SpaceDiscoveryService`] for details.
|
||||
//! See [`SpaceService`] for details.
|
||||
|
||||
use matrix_sdk::{Client, Error};
|
||||
use matrix_sdk::Client;
|
||||
use ruma::OwnedRoomId;
|
||||
use ruma::api::client::space::get_hierarchy;
|
||||
|
||||
pub use crate::spaces::room::SpaceServiceRoom;
|
||||
pub use crate::spaces::{room::SpaceServiceRoom, room_list::SpaceServiceRoomList};
|
||||
|
||||
pub mod room;
|
||||
pub mod room_list;
|
||||
|
||||
pub struct SpaceService {
|
||||
client: Client,
|
||||
@@ -38,36 +38,25 @@ impl SpaceService {
|
||||
.joined_rooms()
|
||||
.into_iter()
|
||||
.filter_map(|room| if room.is_space() { Some(room) } else { None })
|
||||
.map(|room| SpaceServiceRoom::new_from_known(room))
|
||||
.map(SpaceServiceRoom::new_from_known)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub async fn top_level_children_for(
|
||||
&self,
|
||||
space_id: OwnedRoomId,
|
||||
) -> Result<Vec<SpaceServiceRoom>, Error> {
|
||||
let request = get_hierarchy::v1::Request::new(space_id.clone());
|
||||
|
||||
let result = self.client.send(request).await?;
|
||||
|
||||
Ok(result
|
||||
.rooms
|
||||
.iter()
|
||||
.map(|room| (&room.summary, self.client.get_room(&room.summary.room_id)))
|
||||
.map(|(summary, room)| SpaceServiceRoom::new_from_summary(summary, room))
|
||||
.collect::<Vec<_>>())
|
||||
pub fn space_room_list(&self, space_id: OwnedRoomId) -> SpaceServiceRoomList {
|
||||
SpaceServiceRoomList::new(self.client.clone(), space_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
|
||||
use ruma::{RoomVersionId, room_id};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[async_test]
|
||||
async fn test_spaces_hierarchy() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
@@ -80,9 +69,9 @@ mod tests {
|
||||
|
||||
// Given one parent space with 2 children spaces
|
||||
|
||||
let parent_space_id = room_id!("!3:example.org");
|
||||
let child_space_id_1 = room_id!("!1:example.org");
|
||||
let child_space_id_2 = room_id!("!2:example.org");
|
||||
let parent_space_id = room_id!("!parent_space:example.org");
|
||||
let child_space_id_1 = room_id!("!child_space_1:example.org");
|
||||
let child_space_id_2 = room_id!("!child_space_2:example.org");
|
||||
|
||||
server
|
||||
.sync_room(
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::{Room, RoomHero, RoomState};
|
||||
use ruma::events::room::guest_access::GuestAccess;
|
||||
use ruma::events::room::history_visibility::HistoryVisibility;
|
||||
use ruma::room::{JoinRuleSummary, RoomSummary, RoomType};
|
||||
use ruma::{OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId};
|
||||
use ruma::{
|
||||
OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId,
|
||||
events::room::{guest_access::GuestAccess, history_visibility::HistoryVisibility},
|
||||
room::{JoinRuleSummary, RoomSummary, RoomType},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SpaceServiceRoom {
|
||||
pub room_id: OwnedRoomId,
|
||||
pub canonical_alias: Option<OwnedRoomAliasId>,
|
||||
|
||||
223
crates/matrix-sdk-ui/src/spaces/room_list.rs
Normal file
223
crates/matrix-sdk-ui/src/spaces/room_list.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use eyeball::{SharedObservable, Subscriber};
|
||||
use matrix_sdk::{Client, Error, paginators::PaginationToken};
|
||||
use ruma::{OwnedRoomId, api::client::space::get_hierarchy};
|
||||
|
||||
use crate::spaces::SpaceServiceRoom;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SpaceServiceRoomListPaginationState {
|
||||
Idle { end_reached: bool },
|
||||
Loading,
|
||||
}
|
||||
|
||||
pub struct SpaceServiceRoomList {
|
||||
client: Client,
|
||||
|
||||
parent_space_id: OwnedRoomId,
|
||||
|
||||
token: Mutex<PaginationToken>,
|
||||
|
||||
pagination_state: SharedObservable<SpaceServiceRoomListPaginationState>,
|
||||
|
||||
rooms: SharedObservable<Vec<SpaceServiceRoom>>,
|
||||
}
|
||||
|
||||
impl SpaceServiceRoomList {
|
||||
pub fn new(client: Client, parent_space_id: OwnedRoomId) -> Self {
|
||||
Self {
|
||||
client,
|
||||
parent_space_id,
|
||||
token: Mutex::new(None.into()),
|
||||
pagination_state: SharedObservable::new(SpaceServiceRoomListPaginationState::Idle {
|
||||
end_reached: false,
|
||||
}),
|
||||
rooms: SharedObservable::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pagination_state(&self) -> SpaceServiceRoomListPaginationState {
|
||||
self.pagination_state.get()
|
||||
}
|
||||
|
||||
pub fn subscribe_to_pagination_state_updates(
|
||||
&self,
|
||||
) -> Subscriber<SpaceServiceRoomListPaginationState> {
|
||||
self.pagination_state.subscribe()
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> Vec<SpaceServiceRoom> {
|
||||
self.rooms.get()
|
||||
}
|
||||
|
||||
pub fn subscribe_to_room_updates(&self) -> Subscriber<Vec<SpaceServiceRoom>> {
|
||||
self.rooms.subscribe()
|
||||
}
|
||||
|
||||
pub async fn paginate(&self) -> Result<(), Error> {
|
||||
match *self.pagination_state.read() {
|
||||
SpaceServiceRoomListPaginationState::Idle { end_reached } if end_reached => {
|
||||
return Ok(());
|
||||
}
|
||||
SpaceServiceRoomListPaginationState::Loading => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.pagination_state.set(SpaceServiceRoomListPaginationState::Loading);
|
||||
|
||||
let mut request = get_hierarchy::v1::Request::new(self.parent_space_id.clone());
|
||||
|
||||
if let PaginationToken::HasMore(ref token) = *self.token.lock().unwrap() {
|
||||
request.from = Some(token.clone());
|
||||
}
|
||||
|
||||
match self.client.send(request).await {
|
||||
Ok(result) => {
|
||||
let mut token = self.token.lock().unwrap();
|
||||
*token = match &result.next_batch {
|
||||
Some(val) => PaginationToken::HasMore(val.clone()),
|
||||
None => PaginationToken::HitEnd,
|
||||
};
|
||||
|
||||
let mut current_rooms = Vec::new();
|
||||
|
||||
(self.rooms.read()).iter().for_each(|room| {
|
||||
current_rooms.push(room.clone());
|
||||
});
|
||||
|
||||
current_rooms.extend(
|
||||
result
|
||||
.rooms
|
||||
.iter()
|
||||
.map(|room| (&room.summary, self.client.get_room(&room.summary.room_id)))
|
||||
.map(|(summary, room)| SpaceServiceRoom::new_from_summary(summary, room))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
self.rooms.set(current_rooms.clone());
|
||||
|
||||
self.pagination_state.set(SpaceServiceRoomListPaginationState::Idle {
|
||||
end_reached: result.next_batch.is_none(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.pagination_state
|
||||
.set(SpaceServiceRoomListPaginationState::Idle { end_reached: false });
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches2::assert_matches;
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
room::{JoinRuleSummary, RoomSummary},
|
||||
room_id, uint,
|
||||
};
|
||||
use stream_assert::{assert_next_eq, assert_next_matches, assert_pending};
|
||||
|
||||
use crate::spaces::{
|
||||
SpaceService, SpaceServiceRoom, room_list::SpaceServiceRoomListPaginationState,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
async fn test_room_list_pagination() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
let space_service = SpaceService::new(client.clone());
|
||||
|
||||
server.mock_room_state_encryption().plain().mount().await;
|
||||
|
||||
let parent_space_id = room_id!("!parent_space:example.org");
|
||||
|
||||
let room_list = space_service.space_room_list(parent_space_id.to_owned());
|
||||
|
||||
// Start off idle
|
||||
assert_matches!(
|
||||
room_list.pagination_state(),
|
||||
SpaceServiceRoomListPaginationState::Idle { end_reached: false }
|
||||
);
|
||||
|
||||
// without any rooms
|
||||
assert_eq!(room_list.rooms(), vec![]);
|
||||
|
||||
// and with pending subscribers
|
||||
|
||||
let pagination_state_subscriber = room_list.subscribe_to_pagination_state_updates();
|
||||
pin_mut!(pagination_state_subscriber);
|
||||
assert_pending!(pagination_state_subscriber);
|
||||
|
||||
let rooms_subscriber = room_list.subscribe_to_room_updates();
|
||||
pin_mut!(rooms_subscriber);
|
||||
assert_pending!(rooms_subscriber);
|
||||
|
||||
let child_space_id_1 = room_id!("!1:example.org");
|
||||
let child_space_id_2 = room_id!("!2:example.org");
|
||||
|
||||
// Paginating the room list
|
||||
server
|
||||
.mock_get_hierarchy()
|
||||
.ok_with_room_ids(vec![child_space_id_1, child_space_id_2])
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
room_list.paginate().await.unwrap();
|
||||
|
||||
// informs that the pagination reached the end
|
||||
assert_next_matches!(
|
||||
pagination_state_subscriber,
|
||||
SpaceServiceRoomListPaginationState::Idle { end_reached: true }
|
||||
);
|
||||
|
||||
// yields results
|
||||
assert_next_eq!(
|
||||
rooms_subscriber,
|
||||
vec![
|
||||
SpaceServiceRoom::new_from_summary(
|
||||
&RoomSummary::new(
|
||||
child_space_id_1.to_owned(),
|
||||
JoinRuleSummary::Public,
|
||||
false,
|
||||
uint!(1),
|
||||
false,
|
||||
),
|
||||
None,
|
||||
),
|
||||
SpaceServiceRoom::new_from_summary(
|
||||
&RoomSummary::new(
|
||||
child_space_id_2.to_owned(),
|
||||
JoinRuleSummary::Public,
|
||||
false,
|
||||
uint!(1),
|
||||
false,
|
||||
),
|
||||
None,
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1448,6 +1448,13 @@ impl MatrixMockServer {
|
||||
self.mock_endpoint(mock, GetThreadSubscriptionsEndpoint::default())
|
||||
.expect_default_access_token()
|
||||
}
|
||||
|
||||
/// Create a prebuilt mock for the endpoint used to retrieve a space tree
|
||||
pub fn mock_get_hierarchy(&self) -> MockEndpoint<'_, GetHierarchyEndpoint> {
|
||||
let mock =
|
||||
Mock::given(method("GET")).and(path_regex(r"^/_matrix/client/v1/rooms/.*/hierarchy"));
|
||||
self.mock_endpoint(mock, GetHierarchyEndpoint).expect_default_access_token()
|
||||
}
|
||||
}
|
||||
|
||||
/// A specification for a push rule ID.
|
||||
@@ -4210,3 +4217,36 @@ impl<'a> MockEndpoint<'a, GetThreadSubscriptionsEndpoint> {
|
||||
self.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
|
||||
}
|
||||
}
|
||||
|
||||
/// A prebuilt mock for `GET /client/*/rooms/{roomId}/hierarchy`
|
||||
#[derive(Default)]
|
||||
pub struct GetHierarchyEndpoint;
|
||||
|
||||
impl<'a> MockEndpoint<'a, GetHierarchyEndpoint> {
|
||||
/// Returns a successful response containing the given room IDs.
|
||||
pub fn ok_with_room_ids(self, room_ids: Vec<&RoomId>) -> MatrixMock<'a> {
|
||||
let rooms = room_ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
json!({
|
||||
"room_id": id,
|
||||
"num_joined_members": 1,
|
||||
"world_readable": false,
|
||||
"guest_can_join": false,
|
||||
"children_state": []
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"rooms": rooms,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns a successful response with an empty list of rooms.
|
||||
pub fn ok(self) -> MatrixMock<'a> {
|
||||
self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"rooms": []
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user