fractal/session/model/notifications/
notifications_settings.rs1use std::collections::HashMap;
2
3use futures_util::StreamExt;
4use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
5use matrix_sdk::{
6 notification_settings::{
7 IsEncrypted, NotificationSettings as MatrixNotificationSettings, RoomNotificationMode,
8 },
9 NotificationSettingsError,
10};
11use ruma::{
12 push::{PredefinedOverrideRuleId, RuleKind},
13 OwnedRoomId, RoomId,
14};
15use tokio::task::AbortHandle;
16use tokio_stream::wrappers::BroadcastStream;
17use tracing::error;
18
19use crate::{
20 session::model::{Room, Session, SessionState},
21 spawn, spawn_tokio,
22};
23
24#[derive(
26 Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, strum::Display, strum::EnumString,
27)]
28#[enum_type(name = "NotificationsGlobalSetting")]
29#[strum(serialize_all = "kebab-case")]
30pub enum NotificationsGlobalSetting {
31 #[default]
33 All,
34 DirectAndMentions,
36 MentionsOnly,
38}
39
40#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, strum::EnumString)]
42#[enum_type(name = "NotificationsRoomSetting")]
43#[strum(serialize_all = "kebab-case")]
44pub enum NotificationsRoomSetting {
45 #[default]
47 Global,
48 All,
50 MentionsOnly,
52 Mute,
54}
55
56impl NotificationsRoomSetting {
57 fn to_notification_mode(self) -> Option<RoomNotificationMode> {
59 match self {
60 Self::Global => None,
61 Self::All => Some(RoomNotificationMode::AllMessages),
62 Self::MentionsOnly => Some(RoomNotificationMode::MentionsAndKeywordsOnly),
63 Self::Mute => Some(RoomNotificationMode::Mute),
64 }
65 }
66}
67
68impl From<RoomNotificationMode> for NotificationsRoomSetting {
69 fn from(value: RoomNotificationMode) -> Self {
70 match value {
71 RoomNotificationMode::AllMessages => Self::All,
72 RoomNotificationMode::MentionsAndKeywordsOnly => Self::MentionsOnly,
73 RoomNotificationMode::Mute => Self::Mute,
74 }
75 }
76}
77
78mod imp {
79 use std::cell::{Cell, RefCell};
80
81 use super::*;
82
83 #[derive(Debug, Default, glib::Properties)]
84 #[properties(wrapper_type = super::NotificationsSettings)]
85 pub struct NotificationsSettings {
86 #[property(get, set = Self::set_session, explicit_notify, nullable)]
88 session: glib::WeakRef<Session>,
89 api: RefCell<Option<MatrixNotificationSettings>>,
91 #[property(get)]
93 account_enabled: Cell<bool>,
94 #[property(get, set = Self::set_session_enabled, explicit_notify)]
96 session_enabled: Cell<bool>,
97 #[property(get, builder(NotificationsGlobalSetting::default()))]
99 global_setting: Cell<NotificationsGlobalSetting>,
100 #[property(get)]
102 keywords_list: gtk::StringList,
103 per_room_settings: RefCell<HashMap<OwnedRoomId, NotificationsRoomSetting>>,
107 abort_handle: RefCell<Option<AbortHandle>>,
108 }
109
110 #[glib::object_subclass]
111 impl ObjectSubclass for NotificationsSettings {
112 const NAME: &'static str = "NotificationsSettings";
113 type Type = super::NotificationsSettings;
114 }
115
116 #[glib::derived_properties]
117 impl ObjectImpl for NotificationsSettings {
118 fn dispose(&self) {
119 if let Some(handle) = self.abort_handle.take() {
120 handle.abort();
121 }
122 }
123 }
124
125 impl NotificationsSettings {
126 fn set_session(&self, session: Option<&Session>) {
128 if self.session.upgrade().as_ref() == session {
129 return;
130 }
131
132 let obj = self.obj();
133
134 if let Some(session) = session {
135 session
136 .settings()
137 .bind_property("notifications-enabled", &*obj, "session-enabled")
138 .sync_create()
139 .bidirectional()
140 .build();
141 }
142
143 self.session.set(session);
144 obj.notify_session();
145
146 spawn!(clone!(
147 #[weak(rename_to = imp)]
148 self,
149 async move {
150 imp.init_api().await;
151 }
152 ));
153 }
154
155 fn set_session_enabled(&self, enabled: bool) {
157 if self.session_enabled.get() == enabled {
158 return;
159 }
160
161 if !enabled {
162 if let Some(session) = self.session.upgrade() {
163 session.notifications().clear();
164 }
165 }
166
167 self.session_enabled.set(enabled);
168 self.obj().notify_session_enabled();
169 }
170
171 pub(super) fn api(&self) -> Option<MatrixNotificationSettings> {
173 self.api.borrow().clone()
174 }
175
176 async fn init_api(&self) {
178 let Some(session) = self.session.upgrade() else {
179 self.api.take();
180 return;
181 };
182
183 if session.state() != SessionState::Ready {
186 self.api.take();
187
188 session.connect_ready(clone!(
189 #[weak(rename_to = imp)]
190 self,
191 move |_| {
192 spawn!(async move {
193 imp.init_api().await;
194 });
195 }
196 ));
197
198 return;
199 }
200
201 let client = session.client();
202 let api = spawn_tokio!(async move { client.notification_settings().await })
203 .await
204 .expect("task was not aborted");
205 let stream = BroadcastStream::new(api.subscribe_to_changes());
206
207 self.api.replace(Some(api.clone()));
208
209 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
210 let fut = stream.for_each(move |res| {
211 let obj_weak = obj_weak.clone();
212 async move {
213 if res.is_err() {
214 return;
215 }
216
217 let ctx = glib::MainContext::default();
218 ctx.spawn(async move {
219 spawn!(async move {
220 if let Some(obj) = obj_weak.upgrade() {
221 obj.imp().update().await;
222 }
223 });
224 });
225 }
226 });
227
228 self.abort_handle
229 .replace(Some(spawn_tokio!(fut).abort_handle()));
230
231 spawn!(clone!(
232 #[weak(rename_to = imp)]
233 self,
234 async move {
235 imp.update().await;
236 }
237 ));
238 }
239
240 async fn update(&self) {
242 let Some(api) = self.api() else {
243 return;
244 };
245
246 let api_clone = api.clone();
247 let handle = spawn_tokio!(async move {
248 api_clone
249 .is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Master)
250 .await
251 });
252
253 let account_enabled = match handle.await.expect("task was not aborted") {
254 Ok(enabled) => !enabled,
256 Err(error) => {
257 error!("Could not get account notifications setting: {error}");
258 true
259 }
260 };
261 self.set_account_enabled(account_enabled);
262
263 if default_rooms_notifications_is_all(api.clone(), false).await {
264 self.set_global_setting(NotificationsGlobalSetting::All);
265 } else if default_rooms_notifications_is_all(api, true).await {
266 self.set_global_setting(NotificationsGlobalSetting::DirectAndMentions);
267 } else {
268 self.set_global_setting(NotificationsGlobalSetting::MentionsOnly);
269 }
270
271 self.update_keywords_list().await;
272 self.update_per_room_settings().await;
273 }
274
275 pub(super) fn set_account_enabled(&self, enabled: bool) {
277 if self.account_enabled.get() == enabled {
278 return;
279 }
280
281 self.account_enabled.set(enabled);
282 self.obj().notify_account_enabled();
283 }
284
285 pub(super) fn set_global_setting(&self, setting: NotificationsGlobalSetting) {
287 if self.global_setting.get() == setting {
288 return;
289 }
290
291 self.global_setting.set(setting);
292 self.obj().notify_global_setting();
293 }
294
295 pub(super) async fn update_keywords_list(&self) {
297 let Some(api) = self.api() else {
298 return;
299 };
300
301 let keywords = spawn_tokio!(async move { api.enabled_keywords().await })
302 .await
303 .expect("task was not aborted");
304
305 let list = &self.keywords_list;
306 let mut diverges_at = None;
307
308 let keywords = keywords.iter().map(String::as_str).collect::<Vec<_>>();
309 let new_len = keywords.len() as u32;
310 let old_len = list.n_items();
311
312 for (pos, keyword) in keywords.iter().enumerate() {
314 if Some(*keyword)
315 != list
316 .item(pos as u32)
317 .and_downcast::<gtk::StringObject>()
318 .map(|o| o.string())
319 .as_deref()
320 {
321 diverges_at = Some(pos as u32);
322 break;
323 }
324 }
325
326 if diverges_at.is_none() && old_len > new_len {
328 diverges_at = Some(new_len);
329 }
330
331 let Some(pos) = diverges_at else {
332 return;
334 };
335
336 let additions = &keywords[pos as usize..];
337 list.splice(pos, old_len.saturating_sub(pos), additions);
338 }
339
340 pub(super) async fn update_per_room_settings(&self) {
342 let Some(api) = self.api() else {
343 return;
344 };
345
346 let api_clone = api.clone();
347 let room_ids = spawn_tokio!(async move {
348 api_clone
349 .get_rooms_with_user_defined_rules(Some(true))
350 .await
351 })
352 .await
353 .expect("task was not aborted");
354
355 let mut per_room_settings = HashMap::with_capacity(room_ids.len());
357 for room_id in room_ids {
358 let Ok(room_id) = RoomId::parse(room_id) else {
359 continue;
360 };
361
362 let room_id_clone = room_id.clone();
363 let api_clone = api.clone();
364 let handle = spawn_tokio!(async move {
365 api_clone
366 .get_user_defined_room_notification_mode(&room_id_clone)
367 .await
368 });
369
370 if let Some(setting) = handle.await.expect("task was not aborted") {
371 per_room_settings.insert(room_id, setting.into());
372 }
373 }
374
375 self.per_room_settings.replace(per_room_settings.clone());
376
377 let Some(session) = self.session.upgrade() else {
381 return;
382 };
383 let room_list = session.room_list();
384
385 for room in room_list.iter::<Room>() {
386 let Ok(room) = room else {
387 break;
389 };
390
391 if let Some(setting) = per_room_settings.get(room.room_id()) {
392 room.set_notifications_setting(*setting);
393 } else {
394 room.set_notifications_setting(NotificationsRoomSetting::Global);
395 }
396 }
397 }
398 }
399}
400
401glib::wrapper! {
402 pub struct NotificationsSettings(ObjectSubclass<imp::NotificationsSettings>);
404}
405
406impl NotificationsSettings {
407 pub fn new() -> Self {
409 glib::Object::new()
410 }
411
412 pub(crate) async fn set_account_enabled(
414 &self,
415 enabled: bool,
416 ) -> Result<(), NotificationSettingsError> {
417 let imp = self.imp();
418
419 let Some(api) = imp.api() else {
420 error!("Cannot update notifications settings when API is not initialized");
421 return Err(NotificationSettingsError::UnableToUpdatePushRule);
422 };
423
424 let handle = spawn_tokio!(async move {
425 api.set_push_rule_enabled(
426 RuleKind::Override,
427 PredefinedOverrideRuleId::Master,
428 !enabled,
430 )
431 .await
432 });
433
434 match handle.await.expect("task was not aborted") {
435 Ok(()) => {
436 imp.set_account_enabled(enabled);
437 Ok(())
438 }
439 Err(error) => {
440 error!("Could not change account notifications setting: {error}");
441 Err(error)
442 }
443 }
444 }
445
446 pub(crate) async fn set_global_setting(
448 &self,
449 setting: NotificationsGlobalSetting,
450 ) -> Result<(), NotificationSettingsError> {
451 let imp = self.imp();
452
453 let Some(api) = imp.api() else {
454 error!("Cannot update notifications settings when API is not initialized");
455 return Err(NotificationSettingsError::UnableToUpdatePushRule);
456 };
457
458 let (group_all, one_to_one_all) = match setting {
459 NotificationsGlobalSetting::All => (true, true),
460 NotificationsGlobalSetting::DirectAndMentions => (false, true),
461 NotificationsGlobalSetting::MentionsOnly => (false, false),
462 };
463
464 if let Err(error) = set_default_rooms_notifications_all(api.clone(), false, group_all).await
465 {
466 error!("Could not change global group chats notifications setting: {error}");
467 return Err(error);
468 }
469 if let Err(error) = set_default_rooms_notifications_all(api, true, one_to_one_all).await {
470 error!("Could not change global 1-to-1 chats notifications setting: {error}");
471 return Err(error);
472 }
473
474 imp.set_global_setting(setting);
475
476 Ok(())
477 }
478
479 pub(crate) async fn remove_keyword(
481 &self,
482 keyword: String,
483 ) -> Result<(), NotificationSettingsError> {
484 let imp = self.imp();
485
486 let Some(api) = imp.api() else {
487 error!("Cannot update notifications settings when API is not initialized");
488 return Err(NotificationSettingsError::UnableToUpdatePushRule);
489 };
490
491 let keyword_clone = keyword.clone();
492 let handle = spawn_tokio!(async move { api.remove_keyword(&keyword_clone).await });
493
494 if let Err(error) = handle.await.expect("task was not aborted") {
495 error!("Could not remove notification keyword `{keyword}`: {error}");
496 return Err(error);
497 }
498
499 imp.update_keywords_list().await;
500
501 Ok(())
502 }
503
504 pub(crate) async fn add_keyword(
506 &self,
507 keyword: String,
508 ) -> Result<(), NotificationSettingsError> {
509 let imp = self.imp();
510
511 let Some(api) = imp.api() else {
512 error!("Cannot update notifications settings when API is not initialized");
513 return Err(NotificationSettingsError::UnableToUpdatePushRule);
514 };
515
516 let keyword_clone = keyword.clone();
517 let handle = spawn_tokio!(async move { api.add_keyword(keyword_clone).await });
518
519 if let Err(error) = handle.await.expect("task was not aborted") {
520 error!("Could not add notification keyword `{keyword}`: {error}");
521 return Err(error);
522 }
523
524 imp.update_keywords_list().await;
525
526 Ok(())
527 }
528
529 pub(crate) async fn set_per_room_setting(
531 &self,
532 room_id: OwnedRoomId,
533 setting: NotificationsRoomSetting,
534 ) -> Result<(), NotificationSettingsError> {
535 let imp = self.imp();
536
537 let Some(api) = imp.api() else {
538 error!("Cannot update notifications settings when API is not initialized");
539 return Err(NotificationSettingsError::UnableToUpdatePushRule);
540 };
541
542 let room_id_clone = room_id.clone();
543 let handle = if let Some(mode) = setting.to_notification_mode() {
544 spawn_tokio!(async move { api.set_room_notification_mode(&room_id_clone, mode).await })
545 } else {
546 spawn_tokio!(async move { api.delete_user_defined_room_rules(&room_id_clone).await })
547 };
548
549 if let Err(error) = handle.await.expect("task was not aborted") {
550 error!("Could not update notifications setting for room `{room_id}`: {error}");
551 return Err(error);
552 }
553
554 imp.update_per_room_settings().await;
555
556 Ok(())
557 }
558}
559
560impl Default for NotificationsSettings {
561 fn default() -> Self {
562 Self::new()
563 }
564}
565
566async fn default_rooms_notifications_is_all(
567 api: MatrixNotificationSettings,
568 is_one_to_one: bool,
569) -> bool {
570 let mode = spawn_tokio!(async move {
571 api.get_default_room_notification_mode(IsEncrypted::No, is_one_to_one.into())
572 .await
573 })
574 .await
575 .expect("task was not aborted");
576
577 mode == RoomNotificationMode::AllMessages
578}
579
580async fn set_default_rooms_notifications_all(
581 api: MatrixNotificationSettings,
582 is_one_to_one: bool,
583 all: bool,
584) -> Result<(), NotificationSettingsError> {
585 let mode = if all {
586 RoomNotificationMode::AllMessages
587 } else {
588 RoomNotificationMode::MentionsAndKeywordsOnly
589 };
590
591 spawn_tokio!(async move {
592 api.set_default_room_notification_mode(IsEncrypted::No, is_one_to_one.into(), mode)
593 .await
594 })
595 .await
596 .expect("task was not aborted")
597}