fractal/session/remote/
room.rs1use std::{cell::RefCell, time::Duration};
2
3use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
4use matrix_sdk::reqwest::StatusCode;
5use ruma::{
6 OwnedRoomAliasId, OwnedRoomId,
7 api::client::{room::get_summary, space::get_hierarchy},
8 assign,
9 room::{JoinRuleSummary, RoomSummary},
10 uint,
11};
12use tracing::{debug, warn};
13
14use crate::{
15 components::{AvatarImage, AvatarUriSource, PillSource},
16 prelude::*,
17 session::{RoomListRoomInfo, Session},
18 spawn, spawn_tokio,
19 utils::{AbortableHandle, LoadingState, matrix::MatrixRoomIdUri, string::linkify},
20};
21
22const DATA_VALIDITY_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
26
27mod imp {
28 use std::{
29 cell::{Cell, OnceCell},
30 time::Instant,
31 };
32
33 use super::*;
34
35 #[derive(Default, glib::Properties)]
36 #[properties(wrapper_type = super::RemoteRoom)]
37 pub struct RemoteRoom {
38 #[property(get, set = Self::set_session, construct_only)]
40 session: glib::WeakRef<Session>,
41 uri: OnceCell<MatrixRoomIdUri>,
43 room_id: RefCell<Option<OwnedRoomId>>,
45 canonical_alias: RefCell<Option<OwnedRoomAliasId>>,
47 #[property(get)]
52 name: RefCell<Option<String>>,
53 #[property(get)]
55 topic: RefCell<Option<String>>,
56 #[property(get)]
61 topic_linkified: RefCell<Option<String>>,
62 #[property(get)]
64 joined_members_count: Cell<u32>,
65 #[property(get)]
67 can_knock: Cell<bool>,
68 #[property(get)]
70 room_list_info: RoomListRoomInfo,
71 #[property(get, builder(LoadingState::default()))]
73 loading_state: Cell<LoadingState>,
74 last_request_time: Cell<Option<Instant>>,
76 request_abort_handle: AbortableHandle,
77 }
78
79 #[glib::object_subclass]
80 impl ObjectSubclass for RemoteRoom {
81 const NAME: &'static str = "RemoteRoom";
82 type Type = super::RemoteRoom;
83 type ParentType = PillSource;
84 }
85
86 #[glib::derived_properties]
87 impl ObjectImpl for RemoteRoom {}
88
89 impl PillSourceImpl for RemoteRoom {
90 fn identifier(&self) -> String {
91 self.uri().id.to_string()
92 }
93 }
94
95 impl RemoteRoom {
96 fn set_session(&self, session: &Session) {
98 self.session.set(Some(session));
99
100 self.obj().avatar_data().set_image(Some(AvatarImage::new(
101 session,
102 AvatarUriSource::Room,
103 None,
104 None,
105 )));
106
107 self.room_list_info.set_room_list(session.room_list());
108 }
109
110 pub(super) fn set_uri(&self, uri: MatrixRoomIdUri) {
112 if let Ok(room_id) = uri.id.clone().try_into() {
113 self.set_room_id(room_id);
114 }
115
116 self.uri
117 .set(uri)
118 .expect("Matrix URI should be uninitialized");
119
120 self.update_identifiers();
121 self.update_display_name();
122 }
123
124 pub(super) fn uri(&self) -> &MatrixRoomIdUri {
126 self.uri.get().expect("Matrix URI should be initialized")
127 }
128
129 fn set_room_id(&self, room_id: OwnedRoomId) {
131 self.room_id.replace(Some(room_id));
132 }
133
134 pub(super) fn room_id(&self) -> Option<OwnedRoomId> {
136 self.room_id.borrow().clone()
137 }
138
139 fn set_canonical_alias(&self, alias: Option<OwnedRoomAliasId>) {
141 if *self.canonical_alias.borrow() == alias {
142 return;
143 }
144
145 self.canonical_alias.replace(alias);
146 self.update_display_name();
147 }
148
149 pub(super) fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
151 self.canonical_alias
152 .borrow()
153 .clone()
154 .or_else(|| self.uri().id.clone().try_into().ok())
155 }
156
157 fn update_identifiers(&self) {
159 let id = self.uri().id.clone();
160 let room_id = self
161 .room_id()
162 .filter(|room_id| room_id.as_str() != id.as_str())
163 .map(Into::into);
164 let canonical_alias = self
165 .canonical_alias()
166 .filter(|alias| alias.as_str() != id.as_str())
167 .map(Into::into);
168
169 let identifiers = room_id
170 .into_iter()
171 .chain(canonical_alias)
172 .chain(Some(id))
173 .collect();
174
175 self.room_list_info.set_identifiers(identifiers);
176 }
177
178 fn set_name(&self, name: Option<String>) {
180 if *self.name.borrow() == name {
181 return;
182 }
183
184 self.name.replace(name);
185
186 self.obj().notify_name();
187 self.update_display_name();
188 }
189
190 pub(super) fn update_display_name(&self) {
192 let display_name = self
193 .name
194 .borrow()
195 .clone()
196 .or_else(|| {
197 self.canonical_alias
198 .borrow()
199 .as_ref()
200 .map(ToString::to_string)
201 })
202 .unwrap_or_else(|| self.identifier());
203
204 self.obj().set_display_name(display_name);
205 }
206
207 fn set_topic(&self, topic: Option<String>) {
209 let topic =
210 topic.filter(|s| !s.is_empty() && s.find(|c: char| !c.is_whitespace()).is_some());
211
212 if *self.topic.borrow() == topic {
213 return;
214 }
215
216 let topic_linkified = topic.as_deref().map(|t| {
217 let mut s = linkify(t);
219 s.truncate_end_whitespaces();
221 s
222 });
223
224 self.topic.replace(topic);
225 self.topic_linkified.replace(topic_linkified);
226
227 let obj = self.obj();
228 obj.notify_topic();
229 obj.notify_topic_linkified();
230 }
231
232 fn set_joined_members_count(&self, count: u32) {
234 if self.joined_members_count.get() == count {
235 return;
236 }
237
238 self.joined_members_count.set(count);
239 self.obj().notify_joined_members_count();
240 }
241
242 fn set_join_rule(&self, join_rule: &JoinRuleSummary) {
244 let can_knock = matches!(
245 join_rule,
246 JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)
247 );
248
249 if self.can_knock.get() == can_knock {
250 return;
251 }
252
253 self.can_knock.set(can_knock);
254 self.obj().notify_can_knock();
255 }
256
257 pub(super) fn set_loading_state(&self, loading_state: LoadingState) {
259 if self.loading_state.get() == loading_state {
260 return;
261 }
262
263 self.loading_state.set(loading_state);
264
265 if loading_state == LoadingState::Error {
266 self.last_request_time.take();
268 }
269
270 self.obj().notify_loading_state();
271 }
272
273 pub(super) fn set_data(&self, data: RoomSummary) {
275 self.set_room_id(data.room_id);
276 self.set_canonical_alias(data.canonical_alias);
277 self.set_name(data.name.into_clean_string());
278 self.set_topic(data.topic.into_clean_string());
279 self.set_joined_members_count(data.num_joined_members.try_into().unwrap_or(u32::MAX));
280 self.set_join_rule(&data.join_rule);
281
282 if let Some(image) = self.obj().avatar_data().image() {
283 image.set_uri_and_info(data.avatar_url, None);
284 }
285
286 self.update_identifiers();
287 self.set_loading_state(LoadingState::Ready);
288 }
289
290 pub(super) fn is_data_stale(&self) -> bool {
292 self.last_request_time
293 .get()
294 .is_none_or(|last_time| last_time.elapsed() > DATA_VALIDITY_DURATION)
295 }
296
297 pub(super) fn update_last_request_time(&self) {
299 self.last_request_time.set(Some(Instant::now()));
300 }
301
302 pub(super) async fn load_data(&self) {
304 let Some(session) = self.session.upgrade() else {
305 self.last_request_time.take();
306 return;
307 };
308
309 self.set_loading_state(LoadingState::Loading);
310
311 if !self.load_data_from_summary(&session).await {
314 self.load_data_from_space_hierarchy(&session).await;
315 }
316 }
317
318 async fn load_data_from_summary(&self, session: &Session) -> bool {
325 let uri = self.uri();
326 let client = session.client();
327
328 let request = get_summary::v1::Request::new(uri.id.clone(), uri.via.clone());
329 let handle = spawn_tokio!(async move { client.send(request).await });
330
331 let Some(result) = self.request_abort_handle.await_task(handle).await else {
332 return true;
334 };
335
336 match result {
337 Ok(response) => {
338 self.set_data(response.summary);
339 true
340 }
341 Err(error) => {
342 if error
343 .as_client_api_error()
344 .is_some_and(|error| error.status_code == StatusCode::NOT_FOUND)
345 {
346 return false;
347 }
348
349 warn!(
350 "Could not get room details from summary endpoint for room `{}`: {error}",
351 uri.id
352 );
353 self.set_loading_state(LoadingState::Error);
354 true
355 }
356 }
357 }
358
359 async fn load_data_from_space_hierarchy(&self, session: &Session) {
364 let uri = self.uri();
365 let client = session.client();
366
367 let room_id = match OwnedRoomId::try_from(uri.id.clone()) {
369 Ok(room_id) => room_id,
370 Err(alias) => {
371 let client_clone = client.clone();
372 let handle =
373 spawn_tokio!(async move { client_clone.resolve_room_alias(&alias).await });
374
375 let Some(result) = self.request_abort_handle.await_task(handle).await else {
376 return;
378 };
379
380 match result {
381 Ok(response) => response.room_id,
382 Err(error) => {
383 warn!("Could not resolve room alias `{}`: {error}", uri.id);
384 self.set_loading_state(LoadingState::Error);
385 return;
386 }
387 }
388 }
389 };
390
391 let request = assign!(get_hierarchy::v1::Request::new(room_id.clone()), {
392 limit: Some(uint!(1))
394 });
395 let handle = spawn_tokio!(async move { client.send(request).await });
396
397 let Some(result) = self.request_abort_handle.await_task(handle).await else {
398 return;
400 };
401
402 match result {
403 Ok(response) => {
404 if let Some(chunk) = response
405 .rooms
406 .into_iter()
407 .next()
408 .filter(|c| c.summary.room_id == room_id)
409 {
410 self.set_data(chunk.summary);
411 } else {
412 debug!("Space hierarchy endpoint did not return requested room");
413 self.set_loading_state(LoadingState::Error);
414 }
415 }
416 Err(error) => {
417 warn!(
418 "Could not get room details from space hierarchy endpoint for room `{}`: {error}",
419 uri.id
420 );
421 self.set_loading_state(LoadingState::Error);
422 }
423 }
424 }
425 }
426}
427
428glib::wrapper! {
429 pub struct RemoteRoom(ObjectSubclass<imp::RemoteRoom>)
431 @extends PillSource;
432}
433
434impl RemoteRoom {
435 fn without_data(session: &Session, uri: MatrixRoomIdUri) -> Self {
437 let obj = glib::Object::builder::<Self>()
438 .property("session", session)
439 .build();
440 obj.imp().set_uri(uri);
441 obj
442 }
443
444 pub(super) fn new(session: &Session, uri: MatrixRoomIdUri) -> Self {
448 let obj = Self::without_data(session, uri);
449 obj.load_data_if_stale();
450 obj
451 }
452
453 pub(crate) fn with_data(
455 session: &Session,
456 uri: MatrixRoomIdUri,
457 data: impl Into<RoomSummary>,
458 ) -> Self {
459 let obj = Self::without_data(session, uri);
460 obj.imp().set_data(data.into());
461
462 obj
463 }
464
465 pub(crate) fn uri(&self) -> &MatrixRoomIdUri {
467 self.imp().uri()
468 }
469
470 pub(crate) fn room_id(&self) -> Option<OwnedRoomId> {
472 self.imp().room_id()
473 }
474
475 pub(crate) fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
477 self.imp().canonical_alias()
478 }
479
480 pub(super) fn load_data_if_stale(&self) {
482 let imp = self.imp();
483
484 if !imp.is_data_stale() {
485 return;
487 }
488
489 imp.update_last_request_time();
492
493 spawn!(clone!(
494 #[weak]
495 imp,
496 async move {
497 imp.load_data().await;
498 }
499 ));
500 }
501}