notifications-page: Add keywords settings
This commit is contained in:
parent
cd708d637d
commit
8db11bf5a6
|
@ -54,6 +54,9 @@ mod imp {
|
|||
/// The global setting about which messages trigger notifications.
|
||||
#[property(get, builder(NotificationsGlobalSetting::default()))]
|
||||
pub global_setting: Cell<NotificationsGlobalSetting>,
|
||||
/// The list of keywords that trigger notifications.
|
||||
#[property(get)]
|
||||
pub keywords_list: gtk::StringList,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
@ -211,6 +214,11 @@ impl NotificationsSettings {
|
|||
} else {
|
||||
self.set_global_setting_inner(NotificationsGlobalSetting::MentionsOnly);
|
||||
}
|
||||
|
||||
let keywords = spawn_tokio!(async move { api.enabled_keywords().await })
|
||||
.await
|
||||
.unwrap();
|
||||
self.update_keywords_list(&keywords);
|
||||
}
|
||||
|
||||
/// Set whether notifications are enabled for this session.
|
||||
|
@ -293,6 +301,91 @@ impl NotificationsSettings {
|
|||
self.imp().global_setting.set(setting);
|
||||
self.notify_global_setting();
|
||||
}
|
||||
|
||||
/// Update the local list of keywords with the remote one.
|
||||
fn update_keywords_list<'a>(&self, keywords: impl IntoIterator<Item = &'a String>) {
|
||||
let list = &self.imp().keywords_list;
|
||||
let mut diverges_at = None;
|
||||
|
||||
let keywords = keywords.into_iter().map(|s| s.as_str()).collect::<Vec<_>>();
|
||||
let new_len = keywords.len() as u32;
|
||||
let old_len = list.n_items();
|
||||
|
||||
// Check if there is any keyword that changed, was moved or was added.
|
||||
for (pos, keyword) in keywords.iter().enumerate() {
|
||||
if Some(*keyword)
|
||||
!= list
|
||||
.item(pos as u32)
|
||||
.and_downcast::<gtk::StringObject>()
|
||||
.map(|o| o.string())
|
||||
.as_deref()
|
||||
{
|
||||
diverges_at = Some(pos as u32);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if keywords were removed.
|
||||
if diverges_at.is_none() && old_len > new_len {
|
||||
diverges_at = Some(new_len);
|
||||
}
|
||||
|
||||
let Some(pos) = diverges_at else {
|
||||
// Nothing to do.
|
||||
return;
|
||||
};
|
||||
|
||||
let additions = &keywords[pos as usize..];
|
||||
list.splice(pos, old_len.saturating_sub(pos), additions)
|
||||
}
|
||||
|
||||
/// Remove a keyword from the list.
|
||||
pub async fn remove_keyword(&self, keyword: String) -> Result<(), NotificationSettingsError> {
|
||||
let Some(api) = self.api() else {
|
||||
error!("Cannot update notifications settings when API is not initialized");
|
||||
return Err(NotificationSettingsError::UnableToUpdatePushRule);
|
||||
};
|
||||
|
||||
let api_clone = api.clone();
|
||||
let keyword_clone = keyword.clone();
|
||||
let handle = spawn_tokio!(async move { api_clone.remove_keyword(&keyword_clone).await });
|
||||
|
||||
if let Err(error) = handle.await.unwrap() {
|
||||
error!("Failed to remove notification keyword `{keyword}`: {error}");
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let keywords = spawn_tokio!(async move { api.enabled_keywords().await })
|
||||
.await
|
||||
.unwrap();
|
||||
self.update_keywords_list(&keywords);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a keyword to the list.
|
||||
pub async fn add_keyword(&self, keyword: String) -> Result<(), NotificationSettingsError> {
|
||||
let Some(api) = self.api() else {
|
||||
error!("Cannot update notifications settings when API is not initialized");
|
||||
return Err(NotificationSettingsError::UnableToUpdatePushRule);
|
||||
};
|
||||
|
||||
let api_clone = api.clone();
|
||||
let keyword_clone = keyword.clone();
|
||||
let handle = spawn_tokio!(async move { api_clone.add_keyword(keyword_clone).await });
|
||||
|
||||
if let Err(error) = handle.await.unwrap() {
|
||||
error!("Failed to add notification keyword `{keyword}`: {error}");
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let keywords = spawn_tokio!(async move { api.enabled_keywords().await })
|
||||
.await
|
||||
.unwrap();
|
||||
self.update_keywords_list(&keywords);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NotificationsSettings {
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::{glib, glib::clone, CompositeTemplate};
|
||||
use gtk::{gio, glib, glib::clone, CompositeTemplate};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
components::{LoadingBin, Spinner},
|
||||
i18n::gettext_f,
|
||||
session::model::{NotificationsGlobalSetting, NotificationsSettings},
|
||||
spawn, toast,
|
||||
utils::BoundObjectWeakRef,
|
||||
utils::{BoundObjectWeakRef, DummyObject},
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::{cell::Cell, marker::PhantomData};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
|
@ -41,6 +46,13 @@ mod imp {
|
|||
pub global_mentions_bin: TemplateChild<LoadingBin>,
|
||||
#[template_child]
|
||||
pub global_mentions_radio: TemplateChild<gtk::CheckButton>,
|
||||
#[template_child]
|
||||
pub keywords: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub keywords_add_entry: TemplateChild<adw::EntryRow>,
|
||||
#[template_child]
|
||||
pub keywords_add_bin: TemplateChild<LoadingBin>,
|
||||
pub keywords_suffixes: RefCell<HashMap<glib::GString, LoadingBin>>,
|
||||
/// The notifications settings of the current session.
|
||||
#[property(get, set = Self::set_notifications_settings, explicit_notify)]
|
||||
pub notifications_settings: BoundObjectWeakRef<NotificationsSettings>,
|
||||
|
@ -76,7 +88,17 @@ mod imp {
|
|||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for NotificationsPage {}
|
||||
impl ObjectImpl for NotificationsPage {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
self.keywords_add_entry
|
||||
.connect_changed(clone!(@weak obj => move |_| {
|
||||
obj.update_keywords();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for NotificationsPage {}
|
||||
impl PreferencesPageImpl for NotificationsPage {}
|
||||
|
@ -116,6 +138,24 @@ mod imp {
|
|||
global_setting_handler,
|
||||
],
|
||||
);
|
||||
|
||||
let extra_items = gio::ListStore::new::<glib::Object>();
|
||||
extra_items.append(&DummyObject::new("add"));
|
||||
|
||||
let all_items = gio::ListStore::new::<glib::Object>();
|
||||
all_items.append(&settings.keywords_list());
|
||||
all_items.append(&extra_items);
|
||||
|
||||
let flattened_list = gtk::FlattenListModel::new(Some(all_items));
|
||||
self.keywords.bind_model(
|
||||
Some(&flattened_list),
|
||||
clone!(@weak obj => @default-return { adw::ActionRow::new().upcast() }, move |item| obj.create_keyword_row(item)),
|
||||
);
|
||||
} else {
|
||||
self.keywords.bind_model(
|
||||
Option::<&gio::ListModel>::None,
|
||||
clone!(@weak obj => @default-return { adw::ActionRow::new().upcast() }, move |item| obj.create_keyword_row(item)),
|
||||
);
|
||||
}
|
||||
|
||||
obj.update_account();
|
||||
|
@ -186,6 +226,7 @@ impl NotificationsPage {
|
|||
|
||||
// Other sections will be disabled or not.
|
||||
self.update_global();
|
||||
self.update_keywords();
|
||||
}
|
||||
|
||||
/// Update the section about global.
|
||||
|
@ -203,6 +244,24 @@ impl NotificationsPage {
|
|||
imp.global.set_sensitive(sensitive);
|
||||
}
|
||||
|
||||
/// Update the section about keywords.
|
||||
fn update_keywords(&self) {
|
||||
let Some(settings) = self.notifications_settings() else {
|
||||
return;
|
||||
};
|
||||
let imp = self.imp();
|
||||
|
||||
let sensitive = settings.account_enabled() && settings.session_enabled();
|
||||
imp.keywords.set_sensitive(sensitive);
|
||||
|
||||
if !sensitive {
|
||||
// Nothing else to update.
|
||||
return;
|
||||
}
|
||||
|
||||
imp.keywords_add_bin.set_sensitive(self.can_add_keyword());
|
||||
}
|
||||
|
||||
fn set_account_loading(&self, loading: bool) {
|
||||
self.imp().account_loading.set(loading);
|
||||
self.notify_account_loading();
|
||||
|
@ -291,4 +350,145 @@ impl NotificationsPage {
|
|||
obj.update_global();
|
||||
}));
|
||||
}
|
||||
|
||||
/// Create a row in the keywords list for the given item.
|
||||
fn create_keyword_row(&self, item: &glib::Object) -> gtk::Widget {
|
||||
let imp = self.imp();
|
||||
|
||||
if let Some(string_obj) = item.downcast_ref::<gtk::StringObject>() {
|
||||
let keyword = string_obj.string();
|
||||
let row = adw::ActionRow::builder().title(keyword.clone()).build();
|
||||
|
||||
let suffix = LoadingBin::new();
|
||||
let remove_button = gtk::Button::builder()
|
||||
.icon_name("close-symbolic")
|
||||
.valign(gtk::Align::Center)
|
||||
.halign(gtk::Align::Center)
|
||||
.css_classes(["flat"])
|
||||
.tooltip_text(gettext_f("Remove “{keyword}”", &[("keyword", &keyword)]))
|
||||
.build();
|
||||
remove_button.connect_clicked(clone!(@weak self as obj, @weak row => move |_| {
|
||||
obj.remove_keyword(row.title());
|
||||
}));
|
||||
suffix.set_child(Some(remove_button));
|
||||
|
||||
row.add_suffix(&suffix);
|
||||
// We need to keep track of suffixes to change their loading state.
|
||||
imp.keywords_suffixes.borrow_mut().insert(keyword, suffix);
|
||||
|
||||
row.upcast()
|
||||
} else {
|
||||
// It can only be the dummy item to add a new keyword.
|
||||
imp.keywords_add_entry.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the given keyword.
|
||||
fn remove_keyword(&self, keyword: glib::GString) {
|
||||
let Some(settings) = self.notifications_settings() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(suffix) = self.imp().keywords_suffixes.borrow().get(&keyword).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
suffix.set_is_loading(true);
|
||||
|
||||
spawn!(
|
||||
clone!(@weak self as obj, @weak settings, @weak suffix => async move {
|
||||
if settings.remove_keyword(keyword.to_string()).await.is_err() {
|
||||
toast!(
|
||||
obj,
|
||||
gettext("Could not remove notification keyword")
|
||||
);
|
||||
} else {
|
||||
// The row should be removed.
|
||||
obj.imp().keywords_suffixes.borrow_mut().remove(&keyword);
|
||||
}
|
||||
|
||||
suffix.set_is_loading(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether we can add the keyword that is currently in the entry.
|
||||
fn can_add_keyword(&self) -> bool {
|
||||
let imp = self.imp();
|
||||
|
||||
// Cannot add a keyword is section is disabled.
|
||||
if !imp.keywords.is_sensitive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot add a keyword if a keyword is already being added.
|
||||
if imp.keywords_add_bin.is_loading() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let text = imp.keywords_add_entry.text().to_lowercase();
|
||||
|
||||
// Cannot add an empty keyword.
|
||||
if text.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot add a keyword without the API.
|
||||
let Some(settings) = self.notifications_settings() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Cannot add a keyword that already exists.
|
||||
let keywords_list = settings.keywords_list();
|
||||
for keyword_obj in keywords_list.iter::<glib::Object>() {
|
||||
let Ok(keyword_obj) = keyword_obj else {
|
||||
break;
|
||||
};
|
||||
|
||||
if let Some(keyword) = keyword_obj
|
||||
.downcast_ref::<gtk::StringObject>()
|
||||
.map(|o| o.string())
|
||||
{
|
||||
if keyword.to_lowercase() == text {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Add the keyword that is currently in the entry.
|
||||
#[template_callback]
|
||||
fn add_keyword(&self) {
|
||||
let Some(settings) = self.notifications_settings() else {
|
||||
return;
|
||||
};
|
||||
if !self.can_add_keyword() {
|
||||
return;
|
||||
}
|
||||
let imp = self.imp();
|
||||
|
||||
imp.keywords_add_entry.set_sensitive(false);
|
||||
imp.keywords_add_bin.set_is_loading(true);
|
||||
|
||||
spawn!(clone!(@weak self as obj, @weak settings => async move {
|
||||
let imp = obj.imp();
|
||||
let keyword = imp.keywords_add_entry.text().into();
|
||||
|
||||
if settings.add_keyword(keyword).await.is_err() {
|
||||
toast!(
|
||||
obj,
|
||||
gettext("Could not add notification keyword")
|
||||
);
|
||||
} else {
|
||||
// Adding the keyword was successful, reset the entry.
|
||||
imp.keywords_add_entry.set_text("");
|
||||
}
|
||||
|
||||
imp.keywords_add_bin.set_is_loading(false);
|
||||
imp.keywords_add_entry.set_sensitive(true);
|
||||
obj.update_keywords();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,5 +93,38 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Keywords</property>
|
||||
<property name="description" translatable="yes">Messages that contain one of these keywords trigger notifications. Matching on these keywords is case-insensitive.</property>
|
||||
<child>
|
||||
<object class="GtkListBox" id="keywords">
|
||||
<style>
|
||||
<class name="boxed-list"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
<object class="AdwEntryRow" id="keywords_add_entry">
|
||||
<property name="title" translatable="yes">Add Keyword…</property>
|
||||
<signal name="entry-activated" handler="add_keyword" swapped="yes"/>
|
||||
<child>
|
||||
<object class="LoadingBin" id="keywords_add_bin">
|
||||
<property name="child">
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">add-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="tooltip-text" translatable="yes">Add Keyword</property>
|
||||
<signal name="clicked" handler="add_keyword" swapped="true"/>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, glib::Properties)]
|
||||
#[properties(wrapper_type = super::DummyObject)]
|
||||
pub struct DummyObject {
|
||||
/// The identifier of this item.
|
||||
#[property(get, set)]
|
||||
pub id: RefCell<String>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for DummyObject {
|
||||
const NAME: &'static str = "DummyObject";
|
||||
type Type = super::DummyObject;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for DummyObject {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A dummy GObject.
|
||||
///
|
||||
/// It can be used for example to add extra widgets in a list model and can be identified with its ID.
|
||||
pub struct DummyObject(ObjectSubclass<imp::DummyObject>);
|
||||
}
|
||||
|
||||
impl DummyObject {
|
||||
pub fn new(id: &str) -> Self {
|
||||
glib::Object::builder().property("id", id).build()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
//! Collection of common methods and types.
|
||||
|
||||
mod dummy_object;
|
||||
mod expression_list_model;
|
||||
pub mod macros;
|
||||
pub mod matrix;
|
||||
|
@ -27,7 +28,7 @@ use once_cell::sync::{Lazy, OnceCell};
|
|||
use regex::Regex;
|
||||
use tracing::error;
|
||||
|
||||
pub use self::expression_list_model::ExpressionListModel;
|
||||
pub use self::{dummy_object::DummyObject, expression_list_model::ExpressionListModel};
|
||||
use crate::RUNTIME;
|
||||
|
||||
/// Returns an expression that is the and’ed result of the given boolean
|
||||
|
|
Loading…
Reference in New Issue