fractal/session/view/content/room_history/
typing_row.rs

1use adw::subclass::prelude::*;
2use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
3
4use crate::{
5    components::OverlappingAvatars,
6    i18n::{gettext_f, ngettext_f},
7    prelude::*,
8    session::model::{Member, TypingList},
9    utils::BoundObjectWeakRef,
10};
11
12mod imp {
13    use std::marker::PhantomData;
14
15    use glib::subclass::InitializingObject;
16
17    use super::*;
18
19    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
20    #[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/typing_row.ui")]
21    #[properties(wrapper_type = super::TypingRow)]
22    pub struct TypingRow {
23        #[template_child]
24        avatar_list: TemplateChild<OverlappingAvatars>,
25        #[template_child]
26        label: TemplateChild<gtk::Label>,
27        /// The list of members that are currently typing.
28        #[property(get, set = Self::set_list, explicit_notify, nullable)]
29        list: BoundObjectWeakRef<TypingList>,
30        /// Whether the list is empty.
31        #[property(get = Self::is_empty, default = true)]
32        is_empty: PhantomData<bool>,
33    }
34
35    #[glib::object_subclass]
36    impl ObjectSubclass for TypingRow {
37        const NAME: &'static str = "ContentTypingRow";
38        type Type = super::TypingRow;
39        type ParentType = adw::Bin;
40
41        fn class_init(klass: &mut Self::Class) {
42            Self::bind_template(klass);
43
44            klass.set_css_name("typing-row");
45            klass.set_accessible_role(gtk::AccessibleRole::ListItem);
46        }
47
48        fn instance_init(obj: &InitializingObject<Self>) {
49            obj.init_template();
50        }
51    }
52
53    #[glib::derived_properties]
54    impl ObjectImpl for TypingRow {}
55
56    impl WidgetImpl for TypingRow {}
57    impl BinImpl for TypingRow {}
58
59    impl TypingRow {
60        /// Set the list of members that are currently typing.
61        fn set_list(&self, list: Option<&TypingList>) {
62            if self.list.obj().as_ref() == list {
63                return;
64            }
65            let obj = self.obj();
66
67            let prev_is_empty = self.is_empty();
68
69            self.list.disconnect_signals();
70
71            if let Some(list) = list {
72                let items_changed_handler_id = list.connect_items_changed(clone!(
73                    #[weak(rename_to = imp)]
74                    self,
75                    move |list, _pos, removed, added| {
76                        if removed != 0 || added != 0 {
77                            imp.update_label(list);
78                        }
79                    }
80                ));
81                let is_empty_notify_handler_id = list.connect_is_empty_notify(clone!(
82                    #[weak]
83                    obj,
84                    move |_| obj.notify_is_empty()
85                ));
86
87                self.avatar_list.bind_model(Some(list), |item| {
88                    item.downcast_ref::<Member>()
89                        .expect("typing list item should be a member")
90                        .avatar_data()
91                });
92
93                self.list.set(
94                    list,
95                    vec![items_changed_handler_id, is_empty_notify_handler_id],
96                );
97                self.update_label(list);
98            }
99
100            if prev_is_empty != self.is_empty() {
101                obj.notify_is_empty();
102            }
103
104            obj.notify_list();
105        }
106
107        /// Whether the list is empty.
108        fn is_empty(&self) -> bool {
109            let Some(list) = self.list.obj() else {
110                return true;
111            };
112
113            list.is_empty()
114        }
115
116        /// Update the label for the current state of the given typing list.
117        fn update_label(&self, list: &TypingList) {
118            let n = list.n_items();
119            if n == 0 {
120                // Do not update anything, the `is-empty` property should trigger a revealer
121                // animation.
122                return;
123            }
124
125            let label = if n == 1 {
126                let user = list
127                    .item(0)
128                    .and_downcast::<Member>()
129                    .expect("typing list has a member")
130                    .disambiguated_name();
131
132                gettext_f(
133                    // Translators: Do NOT translate the content between '{' and '}', these are
134                    // variable names.
135                    "{user} is typing…",
136                    &[("user", &format!("<b>{user}</b>"))],
137                )
138            } else {
139                ngettext_f(
140                    // Translators: Do NOT translate the content between '{' and '}', these are
141                    // variable names.
142                    "{n} member is typing…",
143                    "{n} members are typing…",
144                    n,
145                    &[("n", &n.to_string())],
146                )
147            };
148            self.label.set_label(&label);
149        }
150    }
151}
152
153glib::wrapper! {
154    /// A widget row used to display typing members.
155    pub struct TypingRow(ObjectSubclass<imp::TypingRow>)
156        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
157}
158
159impl TypingRow {
160    /// Construct a new `TypingRow`.
161    pub fn new() -> Self {
162        glib::Object::new()
163    }
164}
165
166impl Default for TypingRow {
167    fn default() -> Self {
168        Self::new()
169    }
170}