fractal/session/view/account_settings/
notifications_page.rs1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{gio, glib, glib::clone, CompositeTemplate};
4use tracing::error;
5
6use crate::{
7 components::{CheckLoadingRow, EntryAddRow, RemovableRow, SwitchLoadingRow},
8 i18n::gettext_f,
9 session::model::{NotificationsGlobalSetting, NotificationsSettings},
10 spawn, toast,
11 utils::{BoundObjectWeakRef, PlaceholderObject, SingleItemListModel},
12};
13
14mod imp {
15 use std::{cell::Cell, marker::PhantomData};
16
17 use glib::subclass::InitializingObject;
18
19 use super::*;
20
21 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
22 #[template(
23 resource = "/org/gnome/Fractal/ui/session/view/account_settings/notifications_page.ui"
24 )]
25 #[properties(wrapper_type = super::NotificationsPage)]
26 pub struct NotificationsPage {
27 #[template_child]
28 account_row: TemplateChild<SwitchLoadingRow>,
29 #[template_child]
30 session_row: TemplateChild<adw::SwitchRow>,
31 #[template_child]
32 global: TemplateChild<adw::PreferencesGroup>,
33 #[template_child]
34 global_all_row: TemplateChild<CheckLoadingRow>,
35 #[template_child]
36 global_direct_row: TemplateChild<CheckLoadingRow>,
37 #[template_child]
38 global_mentions_row: TemplateChild<CheckLoadingRow>,
39 #[template_child]
40 keywords: TemplateChild<gtk::ListBox>,
41 #[template_child]
42 keywords_add_row: TemplateChild<EntryAddRow>,
43 #[property(get, set = Self::set_notifications_settings, explicit_notify)]
45 notifications_settings: BoundObjectWeakRef<NotificationsSettings>,
46 #[property(get)]
48 account_loading: Cell<bool>,
49 #[property(get)]
51 global_loading: Cell<bool>,
52 #[property(get = Self::global_setting, set = Self::set_global_setting)]
54 global_setting: PhantomData<String>,
55 }
56
57 #[glib::object_subclass]
58 impl ObjectSubclass for NotificationsPage {
59 const NAME: &'static str = "NotificationsPage";
60 type Type = super::NotificationsPage;
61 type ParentType = adw::PreferencesPage;
62
63 fn class_init(klass: &mut Self::Class) {
64 Self::bind_template(klass);
65 Self::bind_template_callbacks(klass);
66
67 klass.install_property_action("notifications.set-global-default", "global-setting");
68 }
69
70 fn instance_init(obj: &InitializingObject<Self>) {
71 obj.init_template();
72 }
73 }
74
75 #[glib::derived_properties]
76 impl ObjectImpl for NotificationsPage {}
77
78 impl WidgetImpl for NotificationsPage {}
79 impl PreferencesPageImpl for NotificationsPage {}
80
81 #[gtk::template_callbacks]
82 impl NotificationsPage {
83 fn set_notifications_settings(
85 &self,
86 notifications_settings: Option<&NotificationsSettings>,
87 ) {
88 if self.notifications_settings.obj().as_ref() == notifications_settings {
89 return;
90 }
91
92 self.notifications_settings.disconnect_signals();
93
94 if let Some(settings) = notifications_settings {
95 let account_enabled_handler = settings.connect_account_enabled_notify(clone!(
96 #[weak(rename_to = imp)]
97 self,
98 move |_| {
99 imp.update_account();
100 }
101 ));
102 let session_enabled_handler = settings.connect_session_enabled_notify(clone!(
103 #[weak(rename_to = imp)]
104 self,
105 move |_| {
106 imp.update_session();
107 }
108 ));
109 let global_setting_handler = settings.connect_global_setting_notify(clone!(
110 #[weak(rename_to = imp)]
111 self,
112 move |_| {
113 imp.update_global();
114 }
115 ));
116
117 self.notifications_settings.set(
118 settings,
119 vec![
120 account_enabled_handler,
121 session_enabled_handler,
122 global_setting_handler,
123 ],
124 );
125
126 let extra_items = SingleItemListModel::new(&PlaceholderObject::new("add"));
127
128 let all_items = gio::ListStore::new::<glib::Object>();
129 all_items.append(&settings.keywords_list());
130 all_items.append(&extra_items);
131
132 let flattened_list = gtk::FlattenListModel::new(Some(all_items));
133 self.keywords.bind_model(
134 Some(&flattened_list),
135 clone!(
136 #[weak(rename_to = imp)]
137 self,
138 #[upgrade_or_else]
139 || { adw::ActionRow::new().upcast() },
140 move |item| imp.create_keyword_row(item)
141 ),
142 );
143 } else {
144 self.keywords.bind_model(
145 None::<&gio::ListModel>,
146 clone!(
147 #[weak(rename_to = imp)]
148 self,
149 #[upgrade_or_else]
150 || { adw::ActionRow::new().upcast() },
151 move |item| imp.create_keyword_row(item)
152 ),
153 );
154 }
155
156 self.update_account();
157 self.obj().notify_notifications_settings();
158 }
159
160 fn global_setting(&self) -> String {
162 let Some(settings) = self.notifications_settings.obj() else {
163 return String::new();
164 };
165
166 settings.global_setting().to_string()
167 }
168
169 fn set_global_setting(&self, default: &str) {
171 let Ok(default) = default.parse::<NotificationsGlobalSetting>() else {
172 error!("Invalid value to set global default notifications setting: {default}");
173 return;
174 };
175
176 spawn!(clone!(
177 #[weak(rename_to = imp)]
178 self,
179 async move {
180 imp.global_setting_changed(default).await;
181 }
182 ));
183 }
184
185 fn update_account(&self) {
187 let Some(settings) = self.notifications_settings.obj() else {
188 return;
189 };
190
191 let checked = settings.account_enabled();
192 self.account_row.set_is_active(checked);
193 self.account_row.set_sensitive(!self.account_loading.get());
194
195 self.update_session();
197 }
198
199 fn update_session(&self) {
201 let Some(settings) = self.notifications_settings.obj() else {
202 return;
203 };
204
205 self.session_row.set_active(settings.session_enabled());
206 self.session_row.set_sensitive(settings.account_enabled());
207
208 self.update_global();
210 self.update_keywords();
211 }
212
213 fn update_global(&self) {
215 let Some(settings) = self.notifications_settings.obj() else {
216 return;
217 };
218
219 self.obj().notify_global_setting();
221
222 let sensitive = settings.account_enabled()
223 && settings.session_enabled()
224 && !self.global_loading.get();
225 self.global.set_sensitive(sensitive);
226 }
227
228 #[template_callback]
230 fn update_keywords(&self) {
231 let Some(settings) = self.notifications_settings.obj() else {
232 return;
233 };
234
235 let sensitive = settings.account_enabled() && settings.session_enabled();
236 self.keywords.set_sensitive(sensitive);
237
238 if !sensitive {
239 return;
241 }
242
243 self.keywords_add_row
244 .set_inhibit_add(!self.can_add_keyword());
245 }
246
247 fn set_account_loading(&self, loading: bool) {
248 self.account_loading.set(loading);
249 self.obj().notify_account_loading();
250 }
251
252 #[template_callback]
253 async fn account_switched(&self) {
254 let Some(settings) = self.notifications_settings.obj() else {
255 return;
256 };
257
258 let enabled = self.account_row.is_active();
259 if enabled == settings.account_enabled() {
260 return;
262 }
263
264 self.account_row.set_sensitive(false);
265 self.set_account_loading(true);
266
267 if settings.set_account_enabled(enabled).await.is_err() {
268 let msg = if enabled {
269 gettext("Could not enable account notifications")
270 } else {
271 gettext("Could not disable account notifications")
272 };
273 toast!(self.obj(), msg);
274 }
275
276 self.set_account_loading(false);
277 self.update_account();
278 }
279
280 #[template_callback]
281 fn session_switched(&self) {
282 let Some(settings) = self.notifications_settings.obj() else {
283 return;
284 };
285
286 settings.set_session_enabled(self.session_row.is_active());
287 }
288
289 fn set_global_loading(&self, loading: bool, setting: NotificationsGlobalSetting) {
290 self.global_all_row
292 .set_is_loading(loading && setting == NotificationsGlobalSetting::All);
293 self.global_direct_row.set_is_loading(
294 loading && setting == NotificationsGlobalSetting::DirectAndMentions,
295 );
296 self.global_mentions_row
297 .set_is_loading(loading && setting == NotificationsGlobalSetting::MentionsOnly);
298
299 self.global_loading.set(loading);
300 self.obj().notify_global_loading();
301 }
302
303 #[template_callback]
304 async fn global_setting_changed(&self, setting: NotificationsGlobalSetting) {
305 let Some(settings) = self.notifications_settings.obj() else {
306 return;
307 };
308
309 if setting == settings.global_setting() {
310 return;
312 }
313
314 self.global.set_sensitive(false);
315 self.set_global_loading(true, setting);
316
317 if settings.set_global_setting(setting).await.is_err() {
318 toast!(
319 self.obj(),
320 gettext("Could not change global notifications setting"),
321 );
322 }
323
324 self.set_global_loading(false, setting);
325 self.update_global();
326 }
327
328 fn create_keyword_row(&self, item: &glib::Object) -> gtk::Widget {
330 let Some(string_obj) = item.downcast_ref::<gtk::StringObject>() else {
331 return self.keywords_add_row.clone().upcast();
333 };
334
335 let keyword = string_obj.string();
336 let row = RemovableRow::new();
337 row.set_title(&keyword);
338 row.set_remove_button_tooltip_text(Some(gettext_f(
339 "Remove “{keyword}”",
340 &[("keyword", &keyword)],
341 )));
342
343 row.connect_remove(clone!(
344 #[weak(rename_to = imp)]
345 self,
346 move |row| {
347 imp.remove_keyword(row);
348 }
349 ));
350
351 row.upcast()
352 }
353
354 fn remove_keyword(&self, row: &RemovableRow) {
356 let Some(settings) = self.notifications_settings.obj() else {
357 return;
358 };
359
360 row.set_is_loading(true);
361
362 let obj = self.obj();
363 spawn!(clone!(
364 #[weak]
365 obj,
366 #[weak]
367 row,
368 async move {
369 if settings.remove_keyword(row.title().into()).await.is_err() {
370 toast!(obj, gettext("Could not remove notification keyword"));
371 }
372
373 row.set_is_loading(false);
374 }
375 ));
376 }
377
378 fn can_add_keyword(&self) -> bool {
380 if !self.keywords.is_sensitive() {
382 return false;
383 }
384
385 if self.keywords_add_row.is_loading() {
387 return false;
388 }
389
390 let text = self.keywords_add_row.text().to_lowercase();
391
392 if text.is_empty() {
394 return false;
395 }
396
397 let Some(settings) = self.notifications_settings.obj() else {
399 return false;
400 };
401
402 let keywords_list = settings.keywords_list();
404 for keyword_obj in keywords_list.iter::<glib::Object>() {
405 let Ok(keyword_obj) = keyword_obj else {
406 break;
407 };
408
409 if let Some(keyword) = keyword_obj
410 .downcast_ref::<gtk::StringObject>()
411 .map(gtk::StringObject::string)
412 {
413 if keyword.to_lowercase() == text {
414 return false;
415 }
416 }
417 }
418
419 true
420 }
421
422 #[template_callback]
424 async fn add_keyword(&self) {
425 if !self.can_add_keyword() {
426 return;
427 }
428
429 let Some(settings) = self.notifications_settings.obj() else {
430 return;
431 };
432
433 self.keywords_add_row.set_is_loading(true);
434
435 let keyword = self.keywords_add_row.text().into();
436
437 if settings.add_keyword(keyword).await.is_err() {
438 toast!(self.obj(), gettext("Could not add notification keyword"));
439 } else {
440 self.keywords_add_row.set_text("");
442 }
443
444 self.keywords_add_row.set_is_loading(false);
445 self.update_keywords();
446 }
447 }
448}
449
450glib::wrapper! {
451 pub struct NotificationsPage(ObjectSubclass<imp::NotificationsPage>)
453 @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
454}
455
456impl NotificationsPage {
457 pub fn new(notifications_settings: &NotificationsSettings) -> Self {
458 glib::Object::builder()
459 .property("notifications-settings", notifications_settings)
460 .build()
461 }
462}