8 changed files with 497 additions and 17 deletions
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="NotificationsPage" parent="AdwPreferencesPage"> |
||||
<property name="icon-name">preferences-system-notifications-symbolic</property> |
||||
<property name="title" translatable="yes">Notifications</property> |
||||
<property name="name">notifications</property> |
||||
<child> |
||||
<object class="AdwPreferencesGroup"> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<property name="title" translatable="yes">Enable for this account</property> |
||||
<child type="suffix"> |
||||
<object class="GtkBox"> |
||||
<property name="valign">center</property> |
||||
<property name="spacing">6</property> |
||||
<child> |
||||
<object class="GtkSpinner"> |
||||
<property name="visible" bind-source="NotificationsPage" bind-property="account-loading" bind-flags="sync-create"/> |
||||
<property name="spinning" bind-source="NotificationsPage" bind-property="account-loading" bind-flags="sync-create"/> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkSwitch"> |
||||
<property name="active" bind-source="NotificationsPage" bind-property="account-enabled" bind-flags="sync-create | bidirectional"/> |
||||
<property name="sensitive" bind-source="NotificationsPage" bind-property="account-loading" bind-flags="sync-create | invert-boolean"/> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwActionRow"> |
||||
<property name="title" translatable="yes">Enable for this session</property> |
||||
<property name="sensitive" bind-source="NotificationsPage" bind-property="account-enabled" bind-flags="sync-create"/> |
||||
<child type="suffix"> |
||||
<object class="GtkSwitch"> |
||||
<property name="valign">center</property> |
||||
<property name="active" bind-source="NotificationsPage" bind-property="session-enabled" bind-flags="sync-create | bidirectional"/> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</template> |
||||
</interface> |
||||
@ -0,0 +1,351 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gettextrs::gettext; |
||||
use gtk::{glib, glib::clone, CompositeTemplate}; |
||||
use log::{error, warn}; |
||||
use matrix_sdk::event_handler::EventHandlerDropGuard; |
||||
use ruma::{ |
||||
api::client::push::{set_pushrule_enabled, RuleKind}, |
||||
events::push_rules::{PushRulesEvent, PushRulesEventContent}, |
||||
push::Ruleset, |
||||
}; |
||||
|
||||
use crate::{session::UserExt, spawn, spawn_tokio, toast, Session}; |
||||
|
||||
const MASTER_RULE_ID: &str = ".m.rule.master"; |
||||
|
||||
mod imp { |
||||
use std::cell::{Cell, RefCell}; |
||||
|
||||
use glib::{subclass::InitializingObject, WeakRef}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)] |
||||
#[template(resource = "/org/gnome/Fractal/account-settings-notifications-page.ui")] |
||||
pub struct NotificationsPage { |
||||
/// The current session.
|
||||
pub session: WeakRef<Session>, |
||||
/// Binding to the session settings `notifications-enabled` property.
|
||||
pub settings_binding: RefCell<Option<glib::Binding>>, |
||||
/// The guard of the event handler for push rules changes.
|
||||
pub event_handler_guard: RefCell<Option<EventHandlerDropGuard>>, |
||||
/// Whether notifications are enabled for this account.
|
||||
pub account_enabled: Cell<bool>, |
||||
/// Whether an account notifications change is being processed.
|
||||
pub account_loading: Cell<bool>, |
||||
/// Whether notifications are enabled for this session.
|
||||
pub session_enabled: Cell<bool>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for NotificationsPage { |
||||
const NAME: &'static str = "NotificationsPage"; |
||||
type Type = super::NotificationsPage; |
||||
type ParentType = adw::PreferencesPage; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for NotificationsPage { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
use once_cell::sync::Lazy; |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![ |
||||
glib::ParamSpecObject::new( |
||||
"session", |
||||
"Session", |
||||
"The session", |
||||
Session::static_type(), |
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, |
||||
), |
||||
glib::ParamSpecBoolean::new( |
||||
"account-enabled", |
||||
"account-enabled", |
||||
"", |
||||
false, |
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, |
||||
), |
||||
glib::ParamSpecBoolean::new( |
||||
"account-loading", |
||||
"account-loading", |
||||
"", |
||||
false, |
||||
glib::ParamFlags::READABLE, |
||||
), |
||||
glib::ParamSpecBoolean::new( |
||||
"session-enabled", |
||||
"session-enabled", |
||||
"", |
||||
false, |
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, |
||||
), |
||||
] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property( |
||||
&self, |
||||
obj: &Self::Type, |
||||
_id: usize, |
||||
value: &glib::Value, |
||||
pspec: &glib::ParamSpec, |
||||
) { |
||||
match pspec.name() { |
||||
"session" => obj.set_session(value.get().unwrap()), |
||||
"account-enabled" => obj.sync_account_enabled(value.get().unwrap()), |
||||
"session-enabled" => obj.set_session_enabled(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
match pspec.name() { |
||||
"session" => obj.session().to_value(), |
||||
"account-enabled" => obj.account_enabled().to_value(), |
||||
"account-loading" => obj.account_loading().to_value(), |
||||
"session-enabled" => obj.session_enabled().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for NotificationsPage {} |
||||
impl PreferencesPageImpl for NotificationsPage {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Preferences page to edit notification settings.
|
||||
pub struct NotificationsPage(ObjectSubclass<imp::NotificationsPage>) |
||||
@extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible; |
||||
} |
||||
|
||||
impl NotificationsPage { |
||||
pub fn new(session: &Session) -> Self { |
||||
glib::Object::new(&[("session", session)]).expect("Failed to create NotificationsPage") |
||||
} |
||||
|
||||
/// The current session.
|
||||
pub fn session(&self) -> Option<Session> { |
||||
self.imp().session.upgrade() |
||||
} |
||||
|
||||
/// Set the current session.
|
||||
pub fn set_session(&self, session: Option<Session>) { |
||||
let prev_session = self.session(); |
||||
if prev_session == session { |
||||
return; |
||||
} |
||||
|
||||
let priv_ = self.imp(); |
||||
if let Some(binding) = priv_.settings_binding.take() { |
||||
binding.unbind(); |
||||
} |
||||
priv_.event_handler_guard.take(); |
||||
|
||||
if let Some(session) = &session { |
||||
let binding = session |
||||
.settings() |
||||
.bind_property("notifications-enabled", self, "session-enabled") |
||||
.flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL) |
||||
.build(); |
||||
priv_.settings_binding.replace(Some(binding)); |
||||
} |
||||
|
||||
priv_.session.set(session.as_ref()); |
||||
self.notify("session"); |
||||
|
||||
spawn!( |
||||
glib::PRIORITY_DEFAULT_IDLE, |
||||
clone!(@weak self as obj => async move { |
||||
obj.init_page().await; |
||||
}) |
||||
); |
||||
} |
||||
|
||||
/// Initialize the page.
|
||||
async fn init_page(&self) { |
||||
let session = match self.session() { |
||||
Some(session) => session, |
||||
None => return, |
||||
}; |
||||
|
||||
let client = session.client(); |
||||
let account = client.account(); |
||||
let handle = |
||||
spawn_tokio!(async move { account.account_data::<PushRulesEventContent>().await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(Some(pushrules)) => match pushrules.deserialize() { |
||||
Ok(pushrules) => { |
||||
self.update_page(pushrules.global); |
||||
} |
||||
Err(error) => { |
||||
error!("Could not deserialize push rules: {error}"); |
||||
toast!( |
||||
self, |
||||
gettext("Could not load notifications settings. Try again later") |
||||
); |
||||
} |
||||
}, |
||||
Ok(None) => { |
||||
warn!("Could not find push rules, using the default ruleset instead."); |
||||
let user_id = session.user().unwrap().user_id(); |
||||
self.update_page(Ruleset::server_default(&user_id)); |
||||
} |
||||
Err(error) => { |
||||
error!("Could not get push rules: {error}"); |
||||
toast!( |
||||
self, |
||||
gettext("Could not load notifications settings. Try again later") |
||||
); |
||||
} |
||||
} |
||||
|
||||
let obj_weak = glib::SendWeakRef::from(self.downgrade()); |
||||
let handler = client.add_event_handler(move |event: PushRulesEvent| { |
||||
let obj_weak = obj_weak.clone(); |
||||
async move { |
||||
let ctx = glib::MainContext::default(); |
||||
ctx.spawn(async move { |
||||
if let Some(obj) = obj_weak.upgrade() { |
||||
obj.update_page(event.content.global) |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
self.imp() |
||||
.event_handler_guard |
||||
.replace(Some(client.event_handler_drop_guard(handler))); |
||||
} |
||||
|
||||
/// Update the page for the given ruleset.
|
||||
fn update_page(&self, rules: Ruleset) { |
||||
let account_enabled = |
||||
if let Some(rule) = rules.override_.iter().find(|r| r.rule_id == MASTER_RULE_ID) { |
||||
!rule.enabled |
||||
} else { |
||||
warn!("Could not find `.m.rule.master` push rule, using the default rule instead."); |
||||
true |
||||
}; |
||||
self.set_account_enabled(account_enabled); |
||||
} |
||||
|
||||
/// Whether notifications are enabled for this account.
|
||||
pub fn account_enabled(&self) -> bool { |
||||
self.imp().account_enabled.get() |
||||
} |
||||
|
||||
/// Set whether notifications are enabled for this account.
|
||||
///
|
||||
/// This only sets the property locally.
|
||||
fn set_account_enabled(&self, enabled: bool) { |
||||
if self.account_enabled() == enabled { |
||||
return; |
||||
} |
||||
|
||||
if !enabled { |
||||
if let Some(session) = self.session() { |
||||
session.clear_notifications(); |
||||
} |
||||
} |
||||
|
||||
self.imp().account_enabled.set(enabled); |
||||
self.notify("account-enabled"); |
||||
} |
||||
|
||||
/// Sync whether notifications are enabled for this account.
|
||||
///
|
||||
/// This sets the property locally and synchronizes the change with the
|
||||
/// homeserver.
|
||||
pub fn sync_account_enabled(&self, enabled: bool) { |
||||
self.set_account_enabled(enabled); |
||||
|
||||
self.set_account_loading(true); |
||||
|
||||
spawn!(clone!(@weak self as obj => async move { |
||||
obj.send_account_enabled(enabled).await; |
||||
})); |
||||
} |
||||
|
||||
/// Send whether notifications are enabled for this account.
|
||||
///
|
||||
/// This only changes the setting on the homeserver.
|
||||
async fn send_account_enabled(&self, enabled: bool) { |
||||
let client = match self.session() { |
||||
Some(session) => session.client(), |
||||
None => return, |
||||
}; |
||||
|
||||
let request = set_pushrule_enabled::v3::Request::new( |
||||
"global", |
||||
RuleKind::Override, |
||||
MASTER_RULE_ID, |
||||
!enabled, |
||||
); |
||||
|
||||
let handle = spawn_tokio!(async move { client.send(request, None).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(_) => {} |
||||
Err(error) => { |
||||
error!("Could not update `{MASTER_RULE_ID}` push rule: {error}"); |
||||
|
||||
let msg = if enabled { |
||||
gettext("Could not enable account notifications") |
||||
} else { |
||||
gettext("Could not disable account notifications") |
||||
}; |
||||
toast!(self, msg); |
||||
|
||||
// Revert the local change.
|
||||
self.set_account_enabled(!enabled); |
||||
} |
||||
} |
||||
|
||||
self.set_account_loading(false); |
||||
} |
||||
|
||||
/// Whether an account notifications change is being processed.
|
||||
pub fn account_loading(&self) -> bool { |
||||
self.imp().account_loading.get() |
||||
} |
||||
|
||||
/// Set whether an account notifications change is being processed.
|
||||
fn set_account_loading(&self, loading: bool) { |
||||
if self.account_loading() == loading { |
||||
return; |
||||
} |
||||
|
||||
self.imp().account_loading.set(loading); |
||||
self.notify("account-loading"); |
||||
} |
||||
|
||||
/// Whether notifications are enabled for this session.
|
||||
pub fn session_enabled(&self) -> bool { |
||||
self.imp().session_enabled.get() |
||||
} |
||||
|
||||
/// Set whether notifications are enabled for this session.
|
||||
pub fn set_session_enabled(&self, enabled: bool) { |
||||
if self.session_enabled() == enabled { |
||||
return; |
||||
} |
||||
|
||||
if !enabled { |
||||
if let Some(session) = self.session() { |
||||
session.clear_notifications(); |
||||
} |
||||
} |
||||
|
||||
self.imp().session_enabled.set(enabled); |
||||
self.notify("session-enabled"); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue