matrix_sdk_ffi/
room_directory_search.rs

1// Copyright 2024 Mauro Romito
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use std::{fmt::Debug, sync::Arc};
17
18use eyeball_im::VectorDiff;
19use futures_util::StreamExt;
20use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
21use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
22use ruma::ServerName;
23use tokio::sync::RwLock;
24
25use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHandle};
26
27#[derive(uniffi::Enum)]
28pub enum PublicRoomJoinRule {
29    Public,
30    Knock,
31    Restricted,
32    KnockRestricted,
33    Invite,
34}
35
36impl TryFrom<ruma::room::JoinRuleKind> for PublicRoomJoinRule {
37    type Error = String;
38
39    fn try_from(value: ruma::room::JoinRuleKind) -> Result<Self, Self::Error> {
40        match value {
41            ruma::room::JoinRuleKind::Public => Ok(Self::Public),
42            ruma::room::JoinRuleKind::Knock => Ok(Self::Knock),
43            ruma::room::JoinRuleKind::Restricted => Ok(Self::Restricted),
44            ruma::room::JoinRuleKind::KnockRestricted => Ok(Self::KnockRestricted),
45            ruma::room::JoinRuleKind::Invite => Ok(Self::Invite),
46            rule => Err(format!("unsupported join rule: {rule:?}")),
47        }
48    }
49}
50
51#[derive(uniffi::Record)]
52pub struct RoomDescription {
53    pub room_id: String,
54    pub name: Option<String>,
55    pub topic: Option<String>,
56    pub alias: Option<String>,
57    pub avatar_url: Option<String>,
58    pub join_rule: Option<PublicRoomJoinRule>,
59    pub is_world_readable: bool,
60    pub joined_members: u64,
61}
62
63impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescription {
64    fn from(value: matrix_sdk::room_directory_search::RoomDescription) -> Self {
65        Self {
66            room_id: value.room_id.to_string(),
67            name: value.name,
68            topic: value.topic,
69            alias: value.alias.map(|alias| alias.to_string()),
70            avatar_url: value.avatar_url.map(|url| url.to_string()),
71            join_rule: value.join_rule.try_into().ok(),
72            is_world_readable: value.is_world_readable,
73            joined_members: value.joined_members,
74        }
75    }
76}
77
78/// A helper for performing room searches in the room directory.
79/// The way this is intended to be used is:
80///
81/// 1. Register a callback using [`RoomDirectorySearch::results`].
82/// 2. Start the room search with [`RoomDirectorySearch::search`].
83/// 3. To get more results, use [`RoomDirectorySearch::next_page`].
84#[derive(uniffi::Object)]
85pub struct RoomDirectorySearch {
86    pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
87}
88
89impl RoomDirectorySearch {
90    pub fn new(inner: SdkRoomDirectorySearch) -> Self {
91        Self { inner: RwLock::new(inner) }
92    }
93}
94
95#[matrix_sdk_ffi_macros::export]
96impl RoomDirectorySearch {
97    /// Asks the server for the next page of the current search.
98    pub async fn next_page(&self) -> Result<(), ClientError> {
99        let mut inner = self.inner.write().await;
100        inner.next_page().await?;
101        Ok(())
102    }
103
104    /// Starts a filtered search for the server.
105    ///
106    /// If the `filter` is not provided it will search for all the rooms.
107    /// You can specify a `batch_size` to control the number of rooms to fetch
108    /// per request.
109    ///
110    /// If the `via_server` is not provided it will search in the current
111    /// homeserver by default.
112    ///
113    /// This method will clear the current search results and start a new one.
114    pub async fn search(
115        &self,
116        filter: Option<String>,
117        batch_size: u32,
118        via_server_name: Option<String>,
119    ) -> Result<(), ClientError> {
120        let server = via_server_name.map(ServerName::parse).transpose()?;
121        let mut inner = self.inner.write().await;
122        inner.search(filter, batch_size, server).await?;
123        Ok(())
124    }
125
126    /// Get the number of pages that have been loaded so far.
127    pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
128        let inner = self.inner.read().await;
129        Ok(inner.loaded_pages() as u32)
130    }
131
132    /// Get whether the search is at the last page.
133    pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
134        let inner = self.inner.read().await;
135        Ok(inner.is_at_last_page())
136    }
137
138    /// Registers a callback to receive new search results when starting a
139    /// search or getting new paginated results.
140    pub async fn results(
141        &self,
142        listener: Box<dyn RoomDirectorySearchEntriesListener>,
143    ) -> Arc<TaskHandle> {
144        let (initial_values, mut stream) = self.inner.read().await.results();
145
146        listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
147            values: initial_values.into_iter().map(Into::into).collect(),
148        }]);
149
150        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
151            while let Some(diffs) = stream.next().await {
152                listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
153            }
154        })))
155    }
156}
157
158#[derive(uniffi::Enum)]
159pub enum RoomDirectorySearchEntryUpdate {
160    Append { values: Vec<RoomDescription> },
161    Clear,
162    PushFront { value: RoomDescription },
163    PushBack { value: RoomDescription },
164    PopFront,
165    PopBack,
166    Insert { index: u32, value: RoomDescription },
167    Set { index: u32, value: RoomDescription },
168    Remove { index: u32 },
169    Truncate { length: u32 },
170    Reset { values: Vec<RoomDescription> },
171}
172
173impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
174    for RoomDirectorySearchEntryUpdate
175{
176    fn from(diff: VectorDiff<matrix_sdk::room_directory_search::RoomDescription>) -> Self {
177        match diff {
178            VectorDiff::Append { values } => {
179                Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
180            }
181            VectorDiff::Clear => Self::Clear,
182            VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
183            VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
184            VectorDiff::PopFront => Self::PopFront,
185            VectorDiff::PopBack => Self::PopBack,
186            VectorDiff::Insert { index, value } => {
187                Self::Insert { index: index as u32, value: value.into() }
188            }
189            VectorDiff::Set { index, value } => {
190                Self::Set { index: index as u32, value: value.into() }
191            }
192            VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
193            VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
194            VectorDiff::Reset { values } => {
195                Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
196            }
197        }
198    }
199}
200
201#[matrix_sdk_ffi_macros::export(callback_interface)]
202pub trait RoomDirectorySearchEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
203    fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
204}