Browse Source

account-settings: Add basic notifications settings page

merge-requests/1327/merge
Kévin Commaille 3 years ago
parent
commit
dffcc460cc
No known key found for this signature in database
GPG Key ID: DD507DAE96E8245C
  1. 1
      data/resources/resources.gresource.xml
  2. 47
      data/resources/ui/account-settings-notifications-page.ui
  3. 5
      data/resources/ui/account-settings.ui
  4. 2
      po/POTFILES.in
  5. 9
      src/session/account_settings/mod.rs
  6. 351
      src/session/account_settings/notifications_page.rs
  7. 5
      src/session/mod.rs
  8. 94
      src/session/settings.rs

1
data/resources/resources.gresource.xml

@ -55,6 +55,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-device-row.ui">ui/account-settings-device-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-devices-page.ui">ui/account-settings-devices-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-import-export-keys-subpage.ui">ui/account-settings-import-export-keys-subpage.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-notifications-page.ui">ui/account-settings-notifications-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-security-page.ui">ui/account-settings-security-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-user-page.ui">ui/account-settings-user-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings.ui">ui/account-settings.ui</file>

47
data/resources/ui/account-settings-notifications-page.ui

@ -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>

5
data/resources/ui/account-settings.ui

@ -9,6 +9,11 @@
<property name="session" bind-source="AccountSettings" bind-property="session" bind-flags="sync-create"/>
</object>
</child>
<child>
<object class="NotificationsPage">
<property name="session" bind-source="AccountSettings" bind-property="session" bind-flags="sync-create"/>
</object>
</child>
<child>
<object class="DevicesPage">
<binding name="user">

2
po/POTFILES.in

@ -10,6 +10,7 @@ data/resources/ui/account-settings-deactivate-account-subpage.ui
data/resources/ui/account-settings-device-row.ui
data/resources/ui/account-settings-devices-page.ui
data/resources/ui/account-settings-import-export-keys-subpage.ui
data/resources/ui/account-settings-notifications-page.ui
data/resources/ui/account-settings-user-page.ui
data/resources/ui/account-settings-security-page.ui
data/resources/ui/account-settings.ui
@ -60,6 +61,7 @@ src/login/mod.rs
src/secret.rs
src/session/account_settings/devices_page/device_list.rs
src/session/account_settings/devices_page/device_row.rs
src/session/account_settings/notifications_page.rs
src/session/account_settings/security_page/import_export_keys_subpage.rs
src/session/account_settings/user_page/change_password_subpage.rs
src/session/account_settings/user_page/deactivate_account_subpage.rs

9
src/session/account_settings/mod.rs

@ -6,12 +6,14 @@ use gtk::{
};
mod devices_page;
mod notifications_page;
mod security_page;
mod user_page;
use devices_page::DevicesPage;
use security_page::SecurityPage;
use user_page::UserPage;
use self::{
devices_page::DevicesPage, notifications_page::NotificationsPage, security_page::SecurityPage,
user_page::UserPage,
};
use super::Session;
mod imp {
@ -37,6 +39,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
DevicesPage::static_type();
UserPage::static_type();
NotificationsPage::static_type();
SecurityPage::static_type();
Self::bind_template(klass);

351
src/session/account_settings/notifications_page.rs

@ -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");
}
}

5
src/session/mod.rs

@ -916,6 +916,11 @@ impl Session {
/// The notification won't be shown if the application is active and this
/// session is displayed.
fn show_notification(&self, matrix_notification: Notification) {
// Don't show notifications if they are disabled.
if !self.settings().notifications_enabled() {
return;
}
let window = self.parent_window().unwrap();
// Don't show notifications for the current session if the window is active.

94
src/session/settings.rs

@ -1,4 +1,5 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use log::error;
use serde::{Deserialize, Serialize};
use crate::Application;
@ -11,22 +12,42 @@ struct StoredSessionSettings {
/// Custom servers to explore.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
explore_custom_servers: Vec<String>,
/// Whether notifications are enabled for this session.
#[serde(
default = "ruma::serde::default_true",
skip_serializing_if = "ruma::serde::is_true"
)]
notifications_enabled: bool,
}
mod imp {
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use once_cell::sync::{Lazy, OnceCell};
use super::*;
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct SessionSettings {
/// The ID of the session these settings are for.
pub session_id: OnceCell<String>,
/// Custom servers to explore.
pub explore_custom_servers: RefCell<Vec<String>>,
/// Whether notifications are enabled for this session.
pub notifications_enabled: Cell<bool>,
}
impl Default for SessionSettings {
fn default() -> Self {
Self {
session_id: Default::default(),
explore_custom_servers: Default::default(),
notifications_enabled: Cell::new(true),
}
}
}
#[glib::object_subclass]
@ -38,13 +59,22 @@ mod imp {
impl ObjectImpl for SessionSettings {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecString::new(
"session-id",
"Session ID",
"The ID of the session these settings are for",
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
)]
vec![
glib::ParamSpecString::new(
"session-id",
"Session ID",
"The ID of the session these settings are for",
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpecBoolean::new(
"notifications-enabled",
"notifications-enabled",
"",
true,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
PROPERTIES.as_ref()
@ -59,6 +89,7 @@ mod imp {
) {
match pspec.name() {
"session-id" => obj.set_session_id(value.get().ok()),
"notifications-enabled" => obj.set_notifications_enabled(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -66,6 +97,7 @@ mod imp {
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"session-id" => obj.session_id().to_value(),
"notifications-enabled" => obj.notifications_enabled().to_value(),
_ => unimplemented!(),
}
}
@ -107,17 +139,35 @@ impl SessionSettings {
priv_.session_id.set(session_id).unwrap();
if let Some(settings) = index.and_then(|idx| sessions.into_iter().nth(idx)) {
*priv_.explore_custom_servers.borrow_mut() = settings.explore_custom_servers;
self.update_from_stored_settings(settings);
} else {
self.store_settings();
}
}
fn store_settings(&self) {
let new_settings = StoredSessionSettings {
fn update_from_stored_settings(&self, settings: StoredSessionSettings) {
let priv_ = self.imp();
let StoredSessionSettings {
session_id: _,
explore_custom_servers,
notifications_enabled,
} = settings;
*priv_.explore_custom_servers.borrow_mut() = explore_custom_servers;
priv_.notifications_enabled.set(notifications_enabled);
}
fn as_stored_settings(&self) -> StoredSessionSettings {
StoredSessionSettings {
session_id: self.session_id().to_owned(),
explore_custom_servers: self.explore_custom_servers(),
};
notifications_enabled: self.notifications_enabled(),
}
}
fn store_settings(&self) {
let new_settings = self.as_stored_settings();
let app_settings = Application::default().settings();
let mut sessions =
serde_json::from_str::<Vec<StoredSessionSettings>>(&app_settings.string("sessions"))
@ -135,7 +185,7 @@ impl SessionSettings {
if let Err(error) =
app_settings.set_string("sessions", &serde_json::to_string(&sessions).unwrap())
{
log::error!("Error storing settings for session: {error}");
error!("Error storing settings for session: {error}");
}
}
@ -180,4 +230,20 @@ impl SessionSettings {
self.imp().explore_custom_servers.replace(servers);
self.store_settings();
}
/// Whether notifications are enabled for this session.
pub fn notifications_enabled(&self) -> bool {
self.imp().notifications_enabled.get()
}
/// Set whether notifications are enabled for this session.
pub fn set_notifications_enabled(&self, enabled: bool) {
if self.notifications_enabled() == enabled {
return;
}
self.imp().notifications_enabled.replace(enabled);
self.store_settings();
self.notify("notifications-enabled");
}
}

Loading…
Cancel
Save