fractal/session_view/explore/
search.rs1use 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
18const 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 #[property(get = Self::list_owned)]
31 list: OnceCell<gio::ListStore>,
32 search: RefCell<ExploreSearchData>,
34 next_batch: RefCell<Option<String>>,
36 #[property(get, builder(LoadingState::default()))]
38 loading_state: Cell<LoadingState>,
39 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 fn list(&self) -> &gio::ListStore {
55 self.list.get_or_init(gio::ListStore::new::<RemoteRoom>)
56 }
57
58 fn list_owned(&self) -> gio::ListStore {
60 self.list().clone()
61 }
62
63 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 spawn!(clone!(
73 #[weak(rename_to = imp)]
74 self,
75 async move {
76 imp.load(true).await;
77 }
78 ));
79 }
80
81 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 pub(super) fn is_empty(&self) -> bool {
93 self.list().n_items() == 0
94 }
95
96 pub(super) fn can_load_more(&self) -> bool {
98 self.loading_state.get() != LoadingState::Loading && self.next_batch.borrow().is_some()
99 }
100
101 pub(super) async fn load(&self, clear: bool) {
106 if !clear && !self.can_load_more() {
109 return;
110 }
111
112 if clear {
113 self.list().remove_all();
115 self.next_batch.take();
116
117 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 self.abort_handle.take();
142 return;
143 };
144
145 self.abort_handle.take();
146
147 if *self.search.borrow() != search {
148 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 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 pub struct ExploreSearch(ObjectSubclass<imp::ExploreSearch>);
197}
198
199impl ExploreSearch {
200 pub fn new() -> Self {
202 glib::Object::new()
203 }
204
205 pub(crate) fn is_empty(&self) -> bool {
207 self.imp().is_empty()
208 }
209
210 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 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#[derive(Debug, Clone, Default, PartialEq)]
251struct ExploreSearchData {
252 session: glib::WeakRef<Session>,
254 search_term: Option<String>,
256 server: Option<OwnedServerName>,
258 third_party_network: Option<String>,
260}
261
262impl ExploreSearchData {
263 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}