fractal/session/view/content/room_history/
typing_row.rs1use 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 #[property(get, set = Self::set_list, explicit_notify, nullable)]
29 list: BoundObjectWeakRef<TypingList>,
30 #[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 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 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 fn update_label(&self, list: &TypingList) {
118 let n = list.n_items();
119 if n == 0 {
120 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 "{user} is typing…",
136 &[("user", &format!("<b>{user}</b>"))],
137 )
138 } else {
139 ngettext_f(
140 "{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 pub struct TypingRow(ObjectSubclass<imp::TypingRow>)
156 @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
157}
158
159impl TypingRow {
160 pub fn new() -> Self {
162 glib::Object::new()
163 }
164}
165
166impl Default for TypingRow {
167 fn default() -> Self {
168 Self::new()
169 }
170}