fractal/components/dialogs/
room_preview.rs1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{glib, glib::clone};
4
5use super::ToastableDialog;
6use crate::{
7 Window,
8 components::{Avatar, LoadingButton},
9 i18n::ngettext_f,
10 prelude::*,
11 session::{RemoteRoom, Session},
12 toast,
13 utils::{
14 LoadingState,
15 matrix::{MatrixIdUri, MatrixRoomIdUri},
16 },
17};
18
19mod imp {
20 use std::cell::{Cell, RefCell};
21
22 use glib::subclass::InitializingObject;
23
24 use super::*;
25
26 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
27 #[template(resource = "/org/gnome/Fractal/ui/components/dialogs/room_preview.ui")]
28 #[properties(wrapper_type = super::RoomPreviewDialog)]
29 pub struct RoomPreviewDialog {
30 #[template_child]
31 go_back_btn: TemplateChild<gtk::Button>,
32 #[template_child]
33 stack: TemplateChild<gtk::Stack>,
34 #[template_child]
35 entry_page: TemplateChild<gtk::Box>,
36 #[template_child]
37 search_entry: TemplateChild<gtk::SearchEntry>,
38 #[template_child]
39 look_up_btn: TemplateChild<LoadingButton>,
40 #[template_child]
41 room_avatar: TemplateChild<Avatar>,
42 #[template_child]
43 room_name: TemplateChild<gtk::Label>,
44 #[template_child]
45 room_alias: TemplateChild<gtk::Label>,
46 #[template_child]
47 room_topic: TemplateChild<gtk::Label>,
48 #[template_child]
49 room_members_box: TemplateChild<gtk::Box>,
50 #[template_child]
51 room_members_count: TemplateChild<gtk::Label>,
52 #[template_child]
53 view_or_join_btn: TemplateChild<LoadingButton>,
54 #[property(get, set = Self::set_session, construct_only)]
56 session: glib::WeakRef<Session>,
57 uri: RefCell<Option<MatrixRoomIdUri>>,
59 #[property(get)]
61 room: RefCell<Option<RemoteRoom>>,
62 disable_go_back: Cell<bool>,
64 room_loading_handler: RefCell<Option<glib::SignalHandlerId>>,
65 room_list_info_handlers: RefCell<Vec<glib::SignalHandlerId>>,
66 }
67
68 #[glib::object_subclass]
69 impl ObjectSubclass for RoomPreviewDialog {
70 const NAME: &'static str = "RoomPreviewDialog";
71 type Type = super::RoomPreviewDialog;
72 type ParentType = ToastableDialog;
73
74 fn class_init(klass: &mut Self::Class) {
75 Self::bind_template(klass);
76 Self::bind_template_callbacks(klass);
77 }
78
79 fn instance_init(obj: &InitializingObject<Self>) {
80 obj.init_template();
81 }
82 }
83
84 #[glib::derived_properties]
85 impl ObjectImpl for RoomPreviewDialog {
86 fn constructed(&self) {
87 self.parent_constructed();
88 let obj = self.obj();
89
90 self.room_topic.connect_activate_link(clone!(
91 #[weak]
92 obj,
93 #[upgrade_or]
94 glib::Propagation::Proceed,
95 move |_, uri| {
96 let Ok(uri) = MatrixIdUri::parse(uri) else {
97 return glib::Propagation::Proceed;
98 };
99 let Some(parent_window) =
100 obj.ancestor(Window::static_type()).and_downcast::<Window>()
101 else {
102 return glib::Propagation::Proceed;
103 };
104
105 parent_window.session_view().show_matrix_uri(uri);
106 glib::Propagation::Stop
107 }
108 ));
109 }
110
111 fn dispose(&self) {
112 self.disconnect_signals();
113 }
114 }
115
116 impl WidgetImpl for RoomPreviewDialog {}
117 impl AdwDialogImpl for RoomPreviewDialog {}
118 impl ToastableDialogImpl for RoomPreviewDialog {}
119
120 #[gtk::template_callbacks]
121 impl RoomPreviewDialog {
122 fn set_session(&self, session: Option<&Session>) {
124 self.session.set(session);
125
126 self.obj().notify_session();
127 self.update_entry_page();
128 }
129
130 pub(super) fn set_uri(&self, uri: MatrixRoomIdUri) {
132 self.uri.replace(Some(uri.clone()));
133 self.disable_go_back(true);
134 self.set_visible_page("loading");
135
136 self.look_up_room_inner(uri);
137 }
138
139 pub(super) fn set_room(&self, room: &RemoteRoom) {
141 if self.room.borrow().as_ref().is_some_and(|r| r == room) {
142 return;
143 }
144
145 self.disconnect_signals();
146
147 let room_list_info = room.room_list_info();
148 let is_joining_handler = room_list_info.connect_is_joining_notify(clone!(
149 #[weak(rename_to = imp)]
150 self,
151 move |_| {
152 imp.update_view_or_join_button();
153 }
154 ));
155 let local_room_handler = room_list_info.connect_local_room_notify(clone!(
156 #[weak(rename_to = imp)]
157 self,
158 move |_| {
159 imp.update_view_or_join_button();
160 }
161 ));
162 self.room_list_info_handlers
163 .replace(vec![is_joining_handler, local_room_handler]);
164
165 self.room.replace(Some(room.clone()));
166
167 if matches!(
168 room.loading_state(),
169 LoadingState::Ready | LoadingState::Error
170 ) {
171 self.fill_details();
172 } else {
173 let room_loading_handler = room.connect_loading_state_notify(clone!(
174 #[weak(rename_to = imp)]
175 self,
176 move |room| {
177 if matches!(
178 room.loading_state(),
179 LoadingState::Ready | LoadingState::Error
180 ) {
181 if let Some(handler) = imp.room_loading_handler.take() {
182 room.disconnect(handler);
183 }
184
185 imp.fill_details();
186 }
187 }
188 ));
189 self.room_loading_handler
190 .replace(Some(room_loading_handler));
191 }
192
193 self.update_view_or_join_button();
194 self.obj().notify_room();
195 }
196
197 pub(super) fn disable_go_back(&self, disable: bool) {
199 self.disable_go_back.set(disable);
200 }
201
202 fn can_go_back(&self) -> bool {
204 !self.disable_go_back.get()
205 && self.stack.visible_child_name().as_deref() == Some("details")
206 }
207
208 fn set_visible_page(&self, page_name: &str) {
210 self.stack.set_visible_child_name(page_name);
211 self.go_back_btn.set_visible(self.can_go_back());
212 }
213
214 #[template_callback]
216 fn update_entry_page(&self) {
217 let Some(session) = self.session.upgrade() else {
218 self.entry_page.set_sensitive(false);
219 return;
220 };
221 self.entry_page.set_sensitive(true);
222
223 let Some(uri) = MatrixRoomIdUri::parse(self.search_entry.text().trim()) else {
224 self.look_up_btn.set_sensitive(false);
225 self.uri.take();
226 return;
227 };
228 self.look_up_btn.set_sensitive(true);
229
230 let id = uri.id.clone();
231 self.uri.replace(Some(uri));
232
233 if session
234 .room_list()
235 .get_by_identifier(&id)
236 .is_some_and(|room| room.is_joined())
237 {
238 self.look_up_btn.set_content_label(gettext("View"));
240 } else {
241 self.look_up_btn.set_content_label(gettext("Look Up"));
243 }
244 }
245
246 #[template_callback]
250 fn look_up_room(&self) {
251 let Some(uri) = self.uri.borrow().clone() else {
252 return;
253 };
254 let obj = self.obj();
255
256 let Some(window) = obj.root().and_downcast::<Window>() else {
257 return;
258 };
259
260 self.look_up_btn.set_is_loading(true);
261 self.entry_page.set_sensitive(false);
262
263 if window.session_view().select_room_if_exists(&uri.id) {
265 obj.close();
266 } else {
267 self.look_up_room_inner(uri);
268 }
269 }
270
271 fn look_up_room_inner(&self, uri: MatrixRoomIdUri) {
272 let Some(session) = self.session.upgrade() else {
273 return;
274 };
275
276 self.go_back_btn.set_sensitive(true);
278
279 let room = session.remote_cache().room(uri);
280 self.set_room(&room);
281 }
282
283 fn fill_details(&self) {
285 let Some(room) = self.room.borrow().clone() else {
286 return;
287 };
288
289 self.room_name.set_label(&room.display_name());
290
291 let alias = room.canonical_alias();
292 if let Some(alias) = &alias {
293 self.room_alias.set_label(alias.as_str());
294 }
295 self.room_alias
296 .set_visible(room.name().is_some() && alias.is_some());
297
298 self.room_avatar.set_data(Some(room.avatar_data()));
299
300 if room.loading_state() == LoadingState::Error {
301 self.room_topic.set_label(&gettext(
302 "The room details cannot be previewed. It can be because the room is not known by the homeserver or because its details are private. You can still try to join it."
303 ));
304 self.room_topic.set_visible(true);
305 self.room_members_box.set_visible(false);
306
307 self.set_visible_page("details");
308 return;
309 }
310
311 if let Some(topic) = room.topic_linkified() {
312 self.room_topic.set_label(&topic);
313 self.room_topic.set_visible(true);
314 } else {
315 self.room_topic.set_visible(false);
316 }
317
318 let members_count = room.joined_members_count();
319 self.room_members_count
320 .set_label(&members_count.to_string());
321
322 let members_tooltip = ngettext_f(
323 "1 member",
326 "{n} members",
327 members_count,
328 &[("n", &members_count.to_string())],
329 );
330 self.room_members_box
331 .set_tooltip_text(Some(&members_tooltip));
332 self.room_members_box.set_visible(true);
333
334 self.update_view_or_join_button();
335 self.set_visible_page("details");
336 }
337
338 fn update_view_or_join_button(&self) {
341 let Some(room) = self.room.borrow().clone() else {
342 return;
343 };
344 let room_list_info = room.room_list_info();
345
346 let label = if room_list_info.local_room().is_some() {
347 gettext("View")
348 } else if room.can_knock() {
349 gettext("Request an Invite")
350 } else {
351 gettext("Join")
352 };
353 self.view_or_join_btn.set_content_label(label);
354 self.view_or_join_btn
355 .set_is_loading(room_list_info.is_joining());
356 }
357
358 #[template_callback]
360 async fn view_or_join_room(&self) {
361 let Some(room) = self.room.borrow().clone() else {
362 return;
363 };
364
365 if let Some(local_room) = room.room_list_info().local_room() {
366 let obj = self.obj();
367
368 if let Some(window) = obj.root().and_downcast_ref::<Window>() {
369 window.session_view().select_room(local_room);
370 obj.close();
371 }
372 } else {
373 self.knock_or_join_room(&room).await;
374 }
375 }
376
377 async fn knock_or_join_room(&self, room: &RemoteRoom) {
379 let Some(session) = self.session.upgrade() else {
380 return;
381 };
382
383 self.go_back_btn.set_sensitive(false);
384
385 let room_list = session.room_list();
387 let uri = room.uri().clone();
388
389 let result = if room.can_knock() {
390 room_list.knock(uri.id, uri.via).await
391 } else {
392 room_list.join_by_id_or_alias(uri.id, uri.via).await
393 };
394
395 match result {
396 Ok(room_id) => {
397 let obj = self.obj();
398
399 if let Some(local_room) = room_list.get_wait(&room_id, None).await
400 && let Some(window) = obj.root().and_downcast_ref::<Window>()
401 {
402 window.session_view().select_room(local_room);
403 }
404
405 obj.close();
406 }
407 Err(error) => {
408 toast!(self.obj(), error);
409
410 self.go_back_btn.set_sensitive(true);
411 }
412 }
413 }
414
415 #[template_callback]
419 fn go_back(&self) {
420 if self.can_go_back() {
421 self.look_up_btn.set_is_loading(false);
423 self.entry_page.set_sensitive(true);
424 self.set_visible_page("entry");
425 } else {
426 self.obj().close();
427 }
428 }
429
430 fn disconnect_signals(&self) {
432 if let Some(room) = self.room.borrow().as_ref() {
433 if let Some(handler) = self.room_loading_handler.take() {
434 room.disconnect(handler);
435 }
436
437 let room_list_info = room.room_list_info();
438 for handler in self.room_list_info_handlers.take() {
439 room_list_info.disconnect(handler);
440 }
441 }
442 }
443 }
444}
445
446glib::wrapper! {
447 pub struct RoomPreviewDialog(ObjectSubclass<imp::RoomPreviewDialog>)
449 @extends gtk::Widget, adw::Dialog, ToastableDialog,
450 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager;
451}
452
453#[gtk::template_callbacks]
454impl RoomPreviewDialog {
455 pub fn new(session: &Session) -> Self {
456 glib::Object::builder().property("session", session).build()
457 }
458
459 pub(crate) fn set_uri(&self, uri: MatrixRoomIdUri) {
461 self.imp().set_uri(uri);
462 }
463
464 pub(crate) fn set_room(&self, room: &RemoteRoom) {
466 let imp = self.imp();
467 imp.disable_go_back(true);
468 imp.set_room(room);
469 }
470}