Skip to main content

fractal/components/dialogs/
room_preview.rs

1use 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        /// The current session.
55        #[property(get, set = Self::set_session, construct_only)]
56        session: glib::WeakRef<Session>,
57        /// The URI to preview.
58        uri: RefCell<Option<MatrixRoomIdUri>>,
59        /// The room that is previewed.
60        #[property(get)]
61        room: RefCell<Option<RemoteRoom>>,
62        /// Whether the "Go back" button is disabled.
63        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        /// Set the current session.
123        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        /// Set the room URI to look up.
131        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        /// Set the room that is previewed.
140        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        /// Set whether to disable the "Go back" button.
198        pub(super) fn disable_go_back(&self, disable: bool) {
199            self.disable_go_back.set(disable);
200        }
201
202        /// Whether we can go back to the previous screen.
203        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        /// Set the currently visible page.
209        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        /// Update the state of the entry page.
215        #[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                // Translators: This is a verb, as in 'View Room'.
239                self.look_up_btn.set_content_label(gettext("View"));
240            } else {
241                // Translators: This is a verb, as in 'Look up Room'.
242                self.look_up_btn.set_content_label(gettext("Look Up"));
243            }
244        }
245
246        /// Look up the room that was entered, if it is valid.
247        ///
248        /// If the room is known, this will open it instead.
249        #[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            // Join or view the room with the given identifier.
264            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            // Reset state before switching to possible pages.
277            self.go_back_btn.set_sensitive(true);
278
279            let room = session.remote_cache().room(uri);
280            self.set_room(&room);
281        }
282
283        /// Fill the details with the given result.
284        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                // Translators: Do NOT translate the content between '{' and '}',
324                // this is a variable name.
325                "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        /// Update the button for viewing or joining the previewed room given
339        /// the current state.
340        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        /// View or join the room that was previewed.
359        #[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        /// Knock or join the room that was previewed, if it is valid.
378        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            // Join the room with the given identifier.
386            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        /// Go back to the previous screen.
416        ///
417        /// If we can't go back, closes the window.
418        #[template_callback]
419        fn go_back(&self) {
420            if self.can_go_back() {
421                // There is only one screen to go back to.
422                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        /// Disconnect the signal handlers of this dialog.
431        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    /// Dialog to preview a room and eventually join it.
448    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    /// Set the room URI to look up.
460    pub(crate) fn set_uri(&self, uri: MatrixRoomIdUri) {
461        self.imp().set_uri(uri);
462    }
463
464    /// Set the room to preview.
465    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}