Skip to main content

fractal/session_view/sidebar/
room_row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{gdk, glib, glib::clone};
4
5use super::SidebarRow;
6use crate::{
7    components::Avatar,
8    i18n::{gettext_f, ngettext_f},
9    prelude::*,
10    session::{HighlightFlags, Room, RoomCategory},
11    utils::{BoundObject, TemplateCallbacks},
12};
13
14mod imp {
15    use glib::subclass::InitializingObject;
16
17    use super::*;
18
19    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
20    #[template(resource = "/org/gnome/Fractal/ui/session_view/sidebar/room_row.ui")]
21    #[properties(wrapper_type = super::SidebarRoomRow)]
22    pub struct SidebarRoomRow {
23        #[template_child]
24        avatar: TemplateChild<Avatar>,
25        #[template_child]
26        display_name_box: TemplateChild<gtk::Box>,
27        #[template_child]
28        room_icon: TemplateChild<gtk::Image>,
29        #[template_child]
30        display_name: TemplateChild<gtk::Label>,
31        #[template_child]
32        notification_count: TemplateChild<gtk::Label>,
33        /// The room represented by this row.
34        #[property(get, set = Self::set_room, explicit_notify, nullable)]
35        room: BoundObject<Room>,
36    }
37
38    #[glib::object_subclass]
39    impl ObjectSubclass for SidebarRoomRow {
40        const NAME: &'static str = "SidebarRoomRow";
41        type Type = super::SidebarRoomRow;
42        type ParentType = adw::Bin;
43
44        fn class_init(klass: &mut Self::Class) {
45            Self::bind_template(klass);
46            TemplateCallbacks::bind_template_callbacks(klass);
47
48            klass.set_css_name("room");
49            klass.set_accessible_role(gtk::AccessibleRole::Group);
50        }
51
52        fn instance_init(obj: &InitializingObject<Self>) {
53            obj.init_template();
54        }
55    }
56
57    #[glib::derived_properties]
58    impl ObjectImpl for SidebarRoomRow {
59        fn constructed(&self) {
60            self.parent_constructed();
61
62            // Allow to drag rooms
63            let drag = gtk::DragSource::builder()
64                .actions(gdk::DragAction::MOVE)
65                .build();
66            drag.connect_prepare(clone!(
67                #[weak(rename_to = imp)]
68                self,
69                #[upgrade_or]
70                None,
71                move |drag, x, y| imp.prepare_drag(drag, x, y)
72            ));
73            drag.connect_drag_begin(clone!(
74                #[weak(rename_to = imp)]
75                self,
76                move |_, _| {
77                    imp.begin_drag();
78                }
79            ));
80            drag.connect_drag_end(clone!(
81                #[weak(rename_to = imp)]
82                self,
83                move |_, _, _| {
84                    imp.end_drag();
85                }
86            ));
87            self.obj().add_controller(drag);
88        }
89    }
90
91    impl WidgetImpl for SidebarRoomRow {}
92    impl BinImpl for SidebarRoomRow {}
93
94    impl SidebarRoomRow {
95        /// Set the room represented by this row.
96        fn set_room(&self, room: Option<Room>) {
97            if self.room.obj() == room {
98                return;
99            }
100
101            self.room.disconnect_signals();
102
103            if let Some(room) = room {
104                let highlight_handler = room.connect_highlight_notify(clone!(
105                    #[weak(rename_to = imp)]
106                    self,
107                    move |_| {
108                        imp.update_highlight();
109                    }
110                ));
111                let direct_handler = room.connect_is_direct_notify(clone!(
112                    #[weak(rename_to = imp)]
113                    self,
114                    move |_| {
115                        imp.update_room_icon();
116                        imp.update_accessibility_label();
117                    }
118                ));
119                let name_handler = room.connect_display_name_notify(clone!(
120                    #[weak(rename_to = imp)]
121                    self,
122                    move |_| {
123                        imp.update_accessibility_label();
124                    }
125                ));
126                let notifications_count_handler = room.connect_notification_count_notify(clone!(
127                    #[weak(rename_to = imp)]
128                    self,
129                    move |_| {
130                        imp.update_accessibility_label();
131                    }
132                ));
133                let category_handler = room.connect_category_notify(clone!(
134                    #[weak(rename_to = imp)]
135                    self,
136                    move |_| {
137                        imp.update_display_name();
138                    }
139                ));
140
141                self.room.set(
142                    room,
143                    vec![
144                        highlight_handler,
145                        direct_handler,
146                        name_handler,
147                        notifications_count_handler,
148                        category_handler,
149                    ],
150                );
151
152                self.update_accessibility_label();
153            }
154
155            self.update_display_name();
156            self.update_highlight();
157            self.update_room_icon();
158            self.obj().notify_room();
159        }
160
161        /// Update the display name of the room according to the current state.
162        fn update_display_name(&self) {
163            let Some(room) = self.room.obj() else {
164                return;
165            };
166
167            if matches!(room.category(), RoomCategory::Left) {
168                self.display_name.add_css_class("dimmed");
169            } else {
170                self.display_name.remove_css_class("dimmed");
171            }
172        }
173
174        /// Update how this row is highlighted according to the current state.
175        fn update_highlight(&self) {
176            if let Some(room) = self.room.obj() {
177                let flags = room.highlight();
178
179                if flags.contains(HighlightFlags::HIGHLIGHT) {
180                    self.notification_count.add_css_class("highlight");
181                } else {
182                    self.notification_count.remove_css_class("highlight");
183                }
184
185                if flags.contains(HighlightFlags::BOLD) {
186                    self.display_name.add_css_class("bold");
187                } else {
188                    self.display_name.remove_css_class("bold");
189                }
190            } else {
191                self.notification_count.remove_css_class("highlight");
192                self.display_name.remove_css_class("bold");
193            }
194        }
195
196        /// The parent `SidebarRow` of this row.
197        fn parent_row(&self) -> Option<SidebarRow> {
198            self.obj().parent().and_downcast()
199        }
200
201        /// Prepare a drag action.
202        fn prepare_drag(
203            &self,
204            drag: &gtk::DragSource,
205            x: f64,
206            y: f64,
207        ) -> Option<gdk::ContentProvider> {
208            let room = self.room.obj()?;
209
210            if let Some(parent) = self.parent_row() {
211                let paintable = gtk::WidgetPaintable::new(Some(&parent));
212                // FIXME: The hotspot coordinates don't work.
213                // See https://gitlab.gnome.org/GNOME/gtk/-/issues/2341
214                drag.set_icon(Some(&paintable), x as i32, y as i32);
215            }
216
217            Some(gdk::ContentProvider::for_value(&room.to_value()))
218        }
219
220        /// Begin a drag action.
221        fn begin_drag(&self) {
222            let Some(room) = self.room.obj() else {
223                return;
224            };
225            let Some(row) = self.parent_row() else {
226                return;
227            };
228            let Some(sidebar) = row.sidebar() else {
229                return;
230            };
231            row.add_css_class("drag");
232
233            sidebar.set_drop_source_category(Some(room.category()));
234        }
235
236        /// End a drag action.
237        fn end_drag(&self) {
238            let Some(row) = self.parent_row() else {
239                return;
240            };
241            let Some(sidebar) = row.sidebar() else {
242                return;
243            };
244            sidebar.set_drop_source_category(None);
245            row.remove_css_class("drag");
246        }
247
248        /// Update the room icon depending on what type of room it is.
249        fn update_room_icon(&self) {
250            let is_direct = self.room.obj().is_some_and(|room| room.is_direct());
251            let is_call = self.room.obj().is_some_and(|room| room.is_call());
252
253            if is_call {
254                self.room_icon.set_icon_name(Some("video-symbolic"));
255                // Translators: A "call room" is a room where an audio or video call
256                // is always active, that users can join and leave at any time.
257                self.room_icon.set_tooltip_text(Some(&gettext("Call room")));
258                self.room_icon.set_visible(true);
259            } else if is_direct {
260                self.room_icon.set_icon_name(Some("person-symbolic"));
261                self.room_icon
262                    .set_tooltip_text(Some(&gettext("Direct chat")));
263                self.room_icon.set_visible(true);
264            } else {
265                self.room_icon.set_visible(false);
266            }
267        }
268
269        /// Update the accessibility label of this row.
270        fn update_accessibility_label(&self) {
271            let Some(parent) = self.obj().parent() else {
272                return;
273            };
274            parent.update_property(&[gtk::accessible::Property::Label(&self.accessible_label())]);
275        }
276
277        /// Compute the accessibility label of this row.
278        fn accessible_label(&self) -> String {
279            let Some(room) = self.room.obj() else {
280                return String::new();
281            };
282
283            let name = if room.is_call() {
284                gettext_f(
285                    // Translators: Do NOT translate the content between '{' and '}', this is a
286                    // variable name. Presented to screen readers for "call rooms".
287                    // A "call room" is a room where an audio or video call is always active,
288                    // that users can join and leave at any time.
289                    "{name} (call room)",
290                    &[("name", &room.display_name())],
291                )
292            } else if room.is_direct() {
293                gettext_f(
294                    // Translators: Do NOT translate the content between '{' and '}', this is a
295                    // variable name. Presented to screen readers when a room is a direct chat with
296                    // another user.
297                    "Direct chat with {name}",
298                    &[("name", &room.display_name())],
299                )
300            } else {
301                room.display_name()
302            };
303
304            if room.notification_count() > 0 {
305                let count = ngettext_f(
306                    // Translators: Do NOT translate the content between '{' and '}', this is a
307                    // variable name. Presented to screen readers when a room has notifications
308                    // for unread messages.
309                    "1 notification",
310                    "{count} notifications",
311                    room.notification_count() as u32,
312                    &[("count", &room.notification_count().to_string())],
313                );
314                format!("{name} {count}")
315            } else {
316                name
317            }
318        }
319    }
320}
321
322glib::wrapper! {
323    /// A sidebar row representing a room.
324    pub struct SidebarRoomRow(ObjectSubclass<imp::SidebarRoomRow>)
325        @extends gtk::Widget, adw::Bin,
326        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
327}
328
329impl SidebarRoomRow {
330    pub fn new() -> Self {
331        glib::Object::new()
332    }
333}
334
335impl Default for SidebarRoomRow {
336    fn default() -> Self {
337        Self::new()
338    }
339}