fractal/session/view/content/explore/
public_room_row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{glib, glib::clone, CompositeTemplate};
4
5use crate::{
6    components::{Avatar, LoadingButton},
7    gettext_f, ngettext_f,
8    prelude::*,
9    session::model::RemoteRoom,
10    toast,
11    utils::{matrix::MatrixIdUri, string::linkify},
12    Window,
13};
14
15mod imp {
16    use std::cell::RefCell;
17
18    use glib::subclass::InitializingObject;
19
20    use super::*;
21
22    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
23    #[template(resource = "/org/gnome/Fractal/ui/session/view/content/explore/public_room_row.ui")]
24    #[properties(wrapper_type = super::PublicRoomRow)]
25    pub struct PublicRoomRow {
26        #[template_child]
27        avatar: TemplateChild<Avatar>,
28        #[template_child]
29        display_name: TemplateChild<gtk::Label>,
30        #[template_child]
31        description: TemplateChild<gtk::Label>,
32        #[template_child]
33        alias: TemplateChild<gtk::Label>,
34        #[template_child]
35        members_count: TemplateChild<gtk::Label>,
36        #[template_child]
37        members_count_box: TemplateChild<gtk::Box>,
38        #[template_child]
39        button: TemplateChild<LoadingButton>,
40        /// The room displayed by this row.
41        #[property(get, set= Self::set_room, explicit_notify)]
42        room: RefCell<Option<RemoteRoom>>,
43        room_list_info_handlers: RefCell<Vec<glib::SignalHandlerId>>,
44    }
45
46    #[glib::object_subclass]
47    impl ObjectSubclass for PublicRoomRow {
48        const NAME: &'static str = "PublicRoomRow";
49        type Type = super::PublicRoomRow;
50        type ParentType = adw::Bin;
51
52        fn class_init(klass: &mut Self::Class) {
53            Self::bind_template(klass);
54            Self::bind_template_callbacks(klass);
55        }
56
57        fn instance_init(obj: &InitializingObject<Self>) {
58            obj.init_template();
59        }
60    }
61
62    #[glib::derived_properties]
63    impl ObjectImpl for PublicRoomRow {
64        fn constructed(&self) {
65            self.parent_constructed();
66            let obj = self.obj();
67
68            self.description.connect_activate_link(clone!(
69                #[weak]
70                obj,
71                #[upgrade_or]
72                glib::Propagation::Proceed,
73                move |_, uri| {
74                    if MatrixIdUri::parse(uri).is_ok() {
75                        let _ =
76                            obj.activate_action("session.show-matrix-uri", Some(&uri.to_variant()));
77                        glib::Propagation::Stop
78                    } else {
79                        glib::Propagation::Proceed
80                    }
81                }
82            ));
83        }
84
85        fn dispose(&self) {
86            self.disconnect_signals();
87        }
88    }
89
90    impl WidgetImpl for PublicRoomRow {}
91    impl BinImpl for PublicRoomRow {}
92
93    #[gtk::template_callbacks]
94    impl PublicRoomRow {
95        /// Set the room displayed by this row.
96        fn set_room(&self, room: RemoteRoom) {
97            if self.room.borrow().as_ref().is_some_and(|r| *r == room) {
98                return;
99            }
100
101            self.disconnect_signals();
102
103            let room_list_info = room.room_list_info();
104            let is_joining_handler = room_list_info.connect_is_joining_notify(clone!(
105                #[weak(rename_to = imp)]
106                self,
107                move |_| {
108                    imp.update_button();
109                }
110            ));
111            let local_room_handler = room_list_info.connect_local_room_notify(clone!(
112                #[weak(rename_to = imp)]
113                self,
114                move |_| {
115                    imp.update_button();
116                }
117            ));
118
119            self.room_list_info_handlers
120                .replace(vec![is_joining_handler, local_room_handler]);
121
122            self.room.replace(Some(room));
123
124            self.update_button();
125            self.update_row();
126            self.obj().notify_room();
127        }
128
129        /// Update this row for the current state.
130        fn update_row(&self) {
131            let Some(room) = self.room.borrow().clone() else {
132                return;
133            };
134
135            self.avatar.set_data(Some(room.avatar_data()));
136            self.display_name.set_text(&room.display_name());
137
138            if let Some(topic) = room.topic() {
139                // Detect links.
140                let mut t = linkify(&topic);
141                // Remove trailing spaces.
142                t.truncate_end_whitespaces();
143
144                self.description.set_label(&t);
145                self.description.set_visible(!t.is_empty());
146            } else {
147                self.description.set_visible(false);
148            }
149
150            let canonical_alias = room.canonical_alias();
151            if let Some(alias) = &canonical_alias {
152                self.alias.set_text(alias.as_str());
153            }
154            self.alias.set_visible(canonical_alias.is_some());
155
156            let members_count = room.joined_members_count();
157            self.members_count.set_text(&members_count.to_string());
158            let members_count_tooltip = ngettext_f(
159                // Translators: Do NOT translate the content between '{' and '}',
160                // this is a variable name.
161                "1 member",
162                "{n} members",
163                members_count,
164                &[("n", &members_count.to_string())],
165            );
166            self.members_count_box
167                .set_tooltip_text(Some(&members_count_tooltip));
168        }
169
170        /// Update the join/view button of this row.
171        fn update_button(&self) {
172            let Some(room) = self.room.borrow().clone() else {
173                return;
174            };
175
176            let room_list_info = room.room_list_info();
177            let room_name = room.display_name();
178
179            let (label, accessible_desc) = if room_list_info.local_room().is_some() {
180                (
181                    // Translators: This is a verb, as in 'View Room'.
182                    gettext("View"),
183                    gettext_f("View {room_name}", &[("room_name", &room_name)]),
184                )
185            } else {
186                (
187                    gettext("Join"),
188                    gettext_f("Join {room_name}", &[("room_name", &room_name)]),
189                )
190            };
191
192            self.button.set_content_label(label);
193            self.button
194                .update_property(&[gtk::accessible::Property::Description(&accessible_desc)]);
195
196            self.button.set_is_loading(room_list_info.is_joining());
197        }
198
199        /// Join or view the public room.
200        #[template_callback]
201        async fn join_or_view(&self) {
202            let Some(room) = self.room.borrow().clone() else {
203                return;
204            };
205
206            let obj = self.obj();
207
208            if let Some(local_room) = room.room_list_info().local_room() {
209                if let Some(window) = obj.root().and_downcast::<Window>() {
210                    window.session_view().select_room(local_room);
211                }
212            } else {
213                let Some(session) = room.session() else {
214                    return;
215                };
216
217                let uri = room.uri();
218
219                if let Err(error) = session
220                    .room_list()
221                    .join_by_id_or_alias(uri.id.clone(), uri.via.clone())
222                    .await
223                {
224                    toast!(obj, error);
225                }
226            }
227        }
228
229        /// Disconnect the signal handlers of this row.
230        fn disconnect_signals(&self) {
231            if let Some(room) = self.room.borrow().as_ref() {
232                let room_list_info = room.room_list_info();
233                for handler in self.room_list_info_handlers.take() {
234                    room_list_info.disconnect(handler);
235                }
236            }
237        }
238    }
239}
240
241glib::wrapper! {
242    /// A row representing a room in a homeserver's public directory.
243    pub struct PublicRoomRow(ObjectSubclass<imp::PublicRoomRow>)
244        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
245}
246
247impl PublicRoomRow {
248    pub fn new() -> Self {
249        glib::Object::new()
250    }
251}
252
253impl Default for PublicRoomRow {
254    fn default() -> Self {
255        Self::new()
256    }
257}