Skip to main content

fractal/session_view/explore/
search.rs

1use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
2use ruma::{
3    OwnedServerName,
4    api::client::directory::get_public_rooms_filtered,
5    assign,
6    directory::{Filter, RoomNetwork, RoomTypeFilter},
7};
8use tokio::task::AbortHandle;
9use tracing::error;
10
11use super::ExploreServer;
12use crate::{
13    session::{RemoteRoom, Session},
14    spawn, spawn_tokio,
15    utils::{LoadingState, matrix::MatrixRoomIdUri},
16};
17
18/// The maximum size of a batch of public rooms.
19const PUBLIC_ROOMS_BATCH_SIZE: u32 = 20;
20
21mod imp {
22    use std::cell::{Cell, OnceCell, RefCell};
23
24    use super::*;
25
26    #[derive(Debug, Default, glib::Properties)]
27    #[properties(wrapper_type = super::ExploreSearch)]
28    pub struct ExploreSearch {
29        /// The list of public rooms for the current search.
30        #[property(get = Self::list_owned)]
31        list: OnceCell<gio::ListStore>,
32        /// The current search.
33        search: RefCell<ExploreSearchData>,
34        /// The next batch to continue the search, if any.
35        next_batch: RefCell<Option<String>>,
36        /// The loading state of the list.
37        #[property(get, builder(LoadingState::default()))]
38        loading_state: Cell<LoadingState>,
39        /// The abort handle for the current request.
40        abort_handle: RefCell<Option<AbortHandle>>,
41    }
42
43    #[glib::object_subclass]
44    impl ObjectSubclass for ExploreSearch {
45        const NAME: &'static str = "ExploreSearch";
46        type Type = super::ExploreSearch;
47    }
48
49    #[glib::derived_properties]
50    impl ObjectImpl for ExploreSearch {}
51
52    impl ExploreSearch {
53        /// The list of public rooms for the current search.
54        fn list(&self) -> &gio::ListStore {
55            self.list.get_or_init(gio::ListStore::new::<RemoteRoom>)
56        }
57
58        /// The owned list of public rooms for the current search.
59        fn list_owned(&self) -> gio::ListStore {
60            self.list().clone()
61        }
62
63        /// Set the current search.
64        pub(super) fn set_search(&self, search: ExploreSearchData) {
65            if *self.search.borrow() == search {
66                return;
67            }
68
69            self.search.replace(search);
70
71            // Trigger a new search.
72            spawn!(clone!(
73                #[weak(rename_to = imp)]
74                self,
75                async move {
76                    imp.load(true).await;
77                }
78            ));
79        }
80
81        /// Set the loading state.
82        fn set_loading_state(&self, state: LoadingState) {
83            if self.loading_state.get() == state {
84                return;
85            }
86
87            self.loading_state.set(state);
88            self.obj().notify_loading_state();
89        }
90
91        /// Whether the list is empty.
92        pub(super) fn is_empty(&self) -> bool {
93            self.list().n_items() == 0
94        }
95
96        /// Whether we can load more rooms with the current search.
97        pub(super) fn can_load_more(&self) -> bool {
98            self.loading_state.get() != LoadingState::Loading && self.next_batch.borrow().is_some()
99        }
100
101        /// Load rooms.
102        ///
103        /// If `clear` is `true`, we start a new search and replace the list of
104        /// rooms, otherwise we use the `next_batch` and add more rooms.
105        pub(super) async fn load(&self, clear: bool) {
106            // Only make a request if we can load more items or we want to replace the
107            // current list.
108            if !clear && !self.can_load_more() {
109                return;
110            }
111
112            if clear {
113                // Clear the list.
114                self.list().remove_all();
115                self.next_batch.take();
116
117                // Abort any ongoing request.
118                if let Some(handle) = self.abort_handle.take() {
119                    handle.abort();
120                }
121            }
122
123            let search = self.search.borrow().clone();
124
125            let Some(session) = search.session.upgrade() else {
126                return;
127            };
128
129            self.set_loading_state(LoadingState::Loading);
130
131            let next_batch = self.next_batch.borrow().clone();
132            let request = search.as_request(next_batch);
133
134            let client = session.client();
135            let handle = spawn_tokio!(async move { client.public_rooms_filtered(request).await });
136
137            self.abort_handle.replace(Some(handle.abort_handle()));
138
139            let Ok(result) = handle.await else {
140                // The request was aborted.
141                self.abort_handle.take();
142                return;
143            };
144
145            self.abort_handle.take();
146
147            if *self.search.borrow() != search {
148                // This is not the current search anymore, ignore the response.
149                return;
150            }
151
152            match result {
153                Ok(response) => self.add_rooms(&session, &search, response),
154                Err(error) => {
155                    self.set_loading_state(LoadingState::Error);
156                    error!("Could not search public rooms: {error}");
157                }
158            }
159        }
160
161        /// Add the rooms from the given response to this list.
162        fn add_rooms(
163            &self,
164            session: &Session,
165            search: &ExploreSearchData,
166            response: get_public_rooms_filtered::v3::Response,
167        ) {
168            self.next_batch.replace(response.next_batch);
169
170            let new_rooms = response
171                .chunk
172                .into_iter()
173                .map(|data| {
174                    let id = data
175                        .canonical_alias
176                        .clone()
177                        .map_or_else(|| data.room_id.clone().into(), Into::into);
178                    let uri = MatrixRoomIdUri {
179                        id,
180                        via: search.server.clone().into_iter().collect(),
181                    };
182
183                    RemoteRoom::with_data(session, uri, data)
184                })
185                .collect::<Vec<_>>();
186
187            self.list().extend_from_slice(&new_rooms);
188
189            self.set_loading_state(LoadingState::Ready);
190        }
191    }
192}
193
194glib::wrapper! {
195    /// The search API of the view to explore rooms in the public directory of homeservers.
196    pub struct ExploreSearch(ObjectSubclass<imp::ExploreSearch>);
197}
198
199impl ExploreSearch {
200    /// Construct a new empty `ExploreSearch`.
201    pub fn new() -> Self {
202        glib::Object::new()
203    }
204
205    /// Whether the list is empty.
206    pub(crate) fn is_empty(&self) -> bool {
207        self.imp().is_empty()
208    }
209
210    /// Search the given term on the given server.
211    pub(crate) fn search(
212        &self,
213        session: &Session,
214        search_term: Option<String>,
215        server: &ExploreServer,
216    ) {
217        let session_weak = glib::WeakRef::new();
218        session_weak.set(Some(session));
219
220        let search = ExploreSearchData {
221            session: session_weak,
222            search_term,
223            server: server.server().cloned(),
224            third_party_network: server.third_party_network(),
225        };
226        self.imp().set_search(search);
227    }
228
229    /// Load more rooms.
230    pub(crate) fn load_more(&self) {
231        let imp = self.imp();
232
233        if imp.can_load_more() {
234            spawn!(clone!(
235                #[weak]
236                imp,
237                async move { imp.load(false).await }
238            ));
239        }
240    }
241}
242
243impl Default for ExploreSearch {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249/// Data about a search in the public rooms directory.
250#[derive(Debug, Clone, Default, PartialEq)]
251struct ExploreSearchData {
252    /// The session to use for performing the search.
253    session: glib::WeakRef<Session>,
254    /// The term to search.
255    search_term: Option<String>,
256    /// The server to search.
257    server: Option<OwnedServerName>,
258    /// The network to search.
259    third_party_network: Option<String>,
260}
261
262impl ExploreSearchData {
263    /// Convert this `ExploreSearchData` to a request.
264    fn as_request(&self, next_batch: Option<String>) -> get_public_rooms_filtered::v3::Request {
265        let room_network = if let Some(third_party_network) = &self.third_party_network {
266            RoomNetwork::ThirdParty(third_party_network.clone())
267        } else {
268            RoomNetwork::Matrix
269        };
270
271        assign!( get_public_rooms_filtered::v3::Request::new(), {
272            limit: Some(PUBLIC_ROOMS_BATCH_SIZE.into()),
273            since: next_batch,
274            room_network,
275            server: self.server.clone(),
276            filter: assign!(
277                Filter::new(),
278                { generic_search_term: self.search_term.clone(), room_types: vec![RoomTypeFilter::Default] }
279            ),
280        })
281    }
282}