[retention] Add setting for auto removing notifications
This commit is contained in:
@ -37,6 +37,10 @@ template $SubscriptionInfoDialog : Adw.Dialog {
|
|||||||
Adw.SwitchRow muted_switch_row {
|
Adw.SwitchRow muted_switch_row {
|
||||||
title: "Muted";
|
title: "Muted";
|
||||||
}
|
}
|
||||||
|
Adw.SpinRow retention_hours_spin_row {
|
||||||
|
title: "Retention Hours";
|
||||||
|
subtitle: "How long messages are stored (0 = forever)";
|
||||||
|
}
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
"boxed-list"
|
"boxed-list"
|
||||||
|
|||||||
1
ntfy-daemon/src/message_repo/migrations/03.sql
Normal file
1
ntfy-daemon/src/message_repo/migrations/03.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE subscription ADD COLUMN retention_hours INTEGER DEFAULT 0;
|
||||||
@ -31,6 +31,7 @@ impl Db {
|
|||||||
conn.execute_batch(include_str!("./migrations/00.sql"))?;
|
conn.execute_batch(include_str!("./migrations/00.sql"))?;
|
||||||
conn.execute_batch(include_str!("./migrations/01.sql"))?;
|
conn.execute_batch(include_str!("./migrations/01.sql"))?;
|
||||||
conn.execute_batch(include_str!("./migrations/02.sql"))?;
|
conn.execute_batch(include_str!("./migrations/02.sql"))?;
|
||||||
|
conn.execute_batch(include_str!("./migrations/03.sql"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn get_or_insert_server(&mut self, server: &str) -> Result<i64> {
|
fn get_or_insert_server(&mut self, server: &str) -> Result<i64> {
|
||||||
@ -56,9 +57,14 @@ impl Db {
|
|||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
pub fn insert_message(&mut self, server: &str, json_data: &str) -> Result<(), Error> {
|
pub fn insert_message(
|
||||||
|
&mut self,
|
||||||
|
server: &str,
|
||||||
|
topic: &str,
|
||||||
|
json_data: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(server)?;
|
let server_id = self.get_or_insert_server(server)?;
|
||||||
let res = self.conn.read().unwrap().execute(
|
let res = self.conn.write().unwrap().execute(
|
||||||
"INSERT INTO message (server, data) VALUES (?1, ?2)",
|
"INSERT INTO message (server, data) VALUES (?1, ?2)",
|
||||||
params![server_id, json_data],
|
params![server_id, json_data],
|
||||||
);
|
);
|
||||||
@ -69,9 +75,48 @@ impl Db {
|
|||||||
Err(Error::DuplicateMessage)
|
Err(Error::DuplicateMessage)
|
||||||
}
|
}
|
||||||
Err(e) => Err(Error::Db(e)),
|
Err(e) => Err(Error::Db(e)),
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => {
|
||||||
|
self.cleanup_messages(server, topic)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_id_from_endpoint(conn: &Connection, endpoint: &str) -> Result<i64> {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT id FROM server WHERE endpoint = ?1",
|
||||||
|
params![endpoint],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_messages(&self, server: &str, topic: &str) -> Result<(), Error> {
|
||||||
|
let cutoff = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let conn = self.conn.read().unwrap();
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT retention_hours FROM subscription sub
|
||||||
|
JOIN server s ON sub.server = s.id
|
||||||
|
WHERE s.endpoint = ?1 AND sub.topic = ?2 AND sub.retention_hours > 0",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let retention_hours: Option<u32> = stmt
|
||||||
|
.query_row(params![server, topic], |row| row.get(0))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if let Some(hours) = retention_hours {
|
||||||
|
let cutoff = cutoff.saturating_sub((hours as u64) * 3600);
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM message WHERE server = ?1 AND topic = ?2 AND timestamp < ?3",
|
||||||
|
params![Self::server_id_from_endpoint(&conn, server)?, topic, cutoff],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_messages(
|
pub fn list_messages(
|
||||||
&self,
|
&self,
|
||||||
server: &str,
|
server: &str,
|
||||||
@ -117,14 +162,15 @@ impl Db {
|
|||||||
pub fn insert_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> {
|
pub fn insert_subscription(&mut self, sub: models::Subscription) -> Result<(), Error> {
|
||||||
let server_id = self.get_or_insert_server(&sub.server)?;
|
let server_id = self.get_or_insert_server(&sub.server)?;
|
||||||
self.conn.read().unwrap().execute(
|
self.conn.read().unwrap().execute(
|
||||||
"INSERT INTO subscription (server, topic, display_name, reserved, muted, archived) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
"INSERT INTO subscription (server, topic, display_name, reserved, muted, archived, retention_hours) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
params![
|
params![
|
||||||
server_id,
|
server_id,
|
||||||
sub.topic,
|
sub.topic,
|
||||||
sub.display_name,
|
sub.display_name,
|
||||||
sub.reserved,
|
sub.reserved,
|
||||||
sub.muted,
|
sub.muted,
|
||||||
sub.archived
|
sub.archived,
|
||||||
|
sub.retention_hours
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -144,7 +190,7 @@ impl Db {
|
|||||||
pub fn list_subscriptions(&mut self) -> Result<Vec<models::Subscription>, Error> {
|
pub fn list_subscriptions(&mut self) -> Result<Vec<models::Subscription>, Error> {
|
||||||
let conn = self.conn.read().unwrap();
|
let conn = self.conn.read().unwrap();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT server.endpoint, sub.topic, sub.display_name, sub.reserved, sub.muted, sub.archived, sub.symbolic_icon, sub.read_until
|
"SELECT server.endpoint, sub.topic, sub.display_name, sub.reserved, sub.muted, sub.archived, sub.symbolic_icon, sub.read_until, sub.retention_hours
|
||||||
FROM subscription sub
|
FROM subscription sub
|
||||||
JOIN server ON server.id = sub.server
|
JOIN server ON server.id = sub.server
|
||||||
ORDER BY server.endpoint, sub.display_name, sub.topic
|
ORDER BY server.endpoint, sub.display_name, sub.topic
|
||||||
@ -160,6 +206,7 @@ impl Db {
|
|||||||
archived: row.get(5)?,
|
archived: row.get(5)?,
|
||||||
symbolic_icon: row.get(6)?,
|
symbolic_icon: row.get(6)?,
|
||||||
read_until: row.get(7)?,
|
read_until: row.get(7)?,
|
||||||
|
retention_hours: row.get(8)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
let subs: Result<Vec<_>, rusqlite::Error> = rows.collect();
|
let subs: Result<Vec<_>, rusqlite::Error> = rows.collect();
|
||||||
@ -170,14 +217,15 @@ impl Db {
|
|||||||
let server_id = self.get_or_insert_server(&sub.server)?;
|
let server_id = self.get_or_insert_server(&sub.server)?;
|
||||||
let res = self.conn.read().unwrap().execute(
|
let res = self.conn.read().unwrap().execute(
|
||||||
"UPDATE subscription
|
"UPDATE subscription
|
||||||
SET display_name = ?1, reserved = ?2, muted = ?3, archived = ?4, read_until = ?5
|
SET display_name = ?1, reserved = ?2, muted = ?3, archived = ?4, read_until = ?5, retention_hours = ?6
|
||||||
WHERE server = ?6 AND topic = ?7",
|
WHERE server = ?7 AND topic = ?8",
|
||||||
params![
|
params![
|
||||||
sub.display_name,
|
sub.display_name,
|
||||||
sub.reserved,
|
sub.reserved,
|
||||||
sub.muted,
|
sub.muted,
|
||||||
sub.archived,
|
sub.archived,
|
||||||
sub.read_until,
|
sub.read_until,
|
||||||
|
sub.retention_hours,
|
||||||
server_id,
|
server_id,
|
||||||
sub.topic,
|
sub.topic,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -177,6 +177,7 @@ pub struct Subscription {
|
|||||||
pub reserved: bool,
|
pub reserved: bool,
|
||||||
pub symbolic_icon: Option<String>,
|
pub symbolic_icon: Option<String>,
|
||||||
pub read_until: u64,
|
pub read_until: u64,
|
||||||
|
pub retention_hours: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Subscription {
|
impl Subscription {
|
||||||
@ -225,6 +226,7 @@ pub struct SubscriptionBuilder {
|
|||||||
reserved: bool,
|
reserved: bool,
|
||||||
symbolic_icon: Option<String>,
|
symbolic_icon: Option<String>,
|
||||||
display_name: String,
|
display_name: String,
|
||||||
|
retention_hours: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubscriptionBuilder {
|
impl SubscriptionBuilder {
|
||||||
@ -237,6 +239,7 @@ impl SubscriptionBuilder {
|
|||||||
reserved: false,
|
reserved: false,
|
||||||
symbolic_icon: None,
|
symbolic_icon: None,
|
||||||
display_name: String::new(),
|
display_name: String::new(),
|
||||||
|
retention_hours: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,6 +273,11 @@ impl SubscriptionBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn retention_hours(mut self, retention_hours: u32) -> Self {
|
||||||
|
self.retention_hours = retention_hours;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Result<Subscription, Error> {
|
pub fn build(self) -> Result<Subscription, Error> {
|
||||||
let res = Subscription {
|
let res = Subscription {
|
||||||
server: self.server,
|
server: self.server,
|
||||||
@ -280,6 +288,7 @@ impl SubscriptionBuilder {
|
|||||||
symbolic_icon: self.symbolic_icon,
|
symbolic_icon: self.symbolic_icon,
|
||||||
display_name: self.display_name,
|
display_name: self.display_name,
|
||||||
read_until: 0,
|
read_until: 0,
|
||||||
|
retention_hours: self.retention_hours,
|
||||||
};
|
};
|
||||||
res.validate()
|
res.validate()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -232,7 +232,11 @@ impl SubscriptionActor {
|
|||||||
// Store in database
|
// Store in database
|
||||||
let already_stored: bool = {
|
let already_stored: bool = {
|
||||||
let json_ev = &serde_json::to_string(&msg).unwrap();
|
let json_ev = &serde_json::to_string(&msg).unwrap();
|
||||||
match self.env.db.insert_message(&self.model.server, json_ev) {
|
match self
|
||||||
|
.env
|
||||||
|
.db
|
||||||
|
.insert_message(&self.model.server, &self.model.topic, json_ev)
|
||||||
|
{
|
||||||
Err(Error::DuplicateMessage) => {
|
Err(Error::DuplicateMessage) => {
|
||||||
warn!(topic=?self.model.topic, "received duplicate message");
|
warn!(topic=?self.model.topic, "received duplicate message");
|
||||||
true
|
true
|
||||||
|
|||||||
@ -54,6 +54,8 @@ mod imp {
|
|||||||
pub muted: Cell<bool>,
|
pub muted: Cell<bool>,
|
||||||
#[property(get)]
|
#[property(get)]
|
||||||
pub unread_count: Cell<u32>,
|
pub unread_count: Cell<u32>,
|
||||||
|
#[property(get)]
|
||||||
|
pub retention_hours: Cell<u32>,
|
||||||
pub read_until: Cell<u64>,
|
pub read_until: Cell<u64>,
|
||||||
pub messages: gio::ListStore,
|
pub messages: gio::ListStore,
|
||||||
pub client: OnceCell<ntfy_daemon::SubscriptionHandle>,
|
pub client: OnceCell<ntfy_daemon::SubscriptionHandle>,
|
||||||
@ -79,6 +81,7 @@ mod imp {
|
|||||||
client: Default::default(),
|
client: Default::default(),
|
||||||
unread_count: Default::default(),
|
unread_count: Default::default(),
|
||||||
read_until: Default::default(),
|
read_until: Default::default(),
|
||||||
|
retention_hours: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,6 +127,7 @@ impl Subscription {
|
|||||||
muted: bool,
|
muted: bool,
|
||||||
read_until: u64,
|
read_until: u64,
|
||||||
display_name: &str,
|
display_name: &str,
|
||||||
|
retention_hours: u32,
|
||||||
) {
|
) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
imp.topic.replace(topic.to_string());
|
imp.topic.replace(topic.to_string());
|
||||||
@ -134,6 +138,8 @@ impl Subscription {
|
|||||||
self.notify_muted();
|
self.notify_muted();
|
||||||
imp.read_until.replace(read_until);
|
imp.read_until.replace(read_until);
|
||||||
self.notify_unread_count();
|
self.notify_unread_count();
|
||||||
|
imp.retention_hours.replace(retention_hours);
|
||||||
|
self.notify_retention_hours();
|
||||||
self._set_display_name(display_name.to_string());
|
self._set_display_name(display_name.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +155,7 @@ impl Subscription {
|
|||||||
model.muted,
|
model.muted,
|
||||||
model.read_until,
|
model.read_until,
|
||||||
&model.display_name,
|
&model.display_name,
|
||||||
|
model.retention_hours,
|
||||||
);
|
);
|
||||||
|
|
||||||
let (prev_msgs, mut rx) = remote_subscription.attach().await;
|
let (prev_msgs, mut rx) = remote_subscription.attach().await;
|
||||||
@ -214,8 +221,9 @@ impl Subscription {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.update_info(
|
.update_info(
|
||||||
models::Subscription::builder(self.topic())
|
models::Subscription::builder(self.topic())
|
||||||
.display_name((imp.display_name.borrow().to_string()))
|
.display_name(imp.display_name.borrow().to_string())
|
||||||
.muted(imp.muted.get())
|
.muted(imp.muted.get())
|
||||||
|
.retention_hours(imp.retention_hours.get())
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| anyhow::anyhow!("invalid subscription data {:?}", e))?,
|
.map_err(|e| anyhow::anyhow!("invalid subscription data {:?}", e))?,
|
||||||
)
|
)
|
||||||
@ -249,6 +257,15 @@ impl Subscription {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn set_retention_hours(&self, value: u32) -> impl Future<Output = anyhow::Result<()>> {
|
||||||
|
let this = self.clone();
|
||||||
|
async move {
|
||||||
|
this.imp().retention_hours.replace(value);
|
||||||
|
this.notify_retention_hours();
|
||||||
|
this.send_updated_info().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
pub async fn flag_all_as_read(&self) -> anyhow::Result<()> {
|
pub async fn flag_all_as_read(&self) -> anyhow::Result<()> {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let Some(value) = Self::last_message(&imp.messages)
|
let Some(value) = Self::last_message(&imp.messages)
|
||||||
|
|||||||
@ -20,6 +20,8 @@ mod imp {
|
|||||||
pub display_name_entry: TemplateChild<adw::EntryRow>,
|
pub display_name_entry: TemplateChild<adw::EntryRow>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub muted_switch_row: TemplateChild<adw::SwitchRow>,
|
pub muted_switch_row: TemplateChild<adw::SwitchRow>,
|
||||||
|
#[template_child]
|
||||||
|
pub retention_hours_spin_row: TemplateChild<adw::SpinRow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@ -47,6 +49,12 @@ mod imp {
|
|||||||
.set_text(&this.subscription().unwrap().display_name());
|
.set_text(&this.subscription().unwrap().display_name());
|
||||||
self.muted_switch_row
|
self.muted_switch_row
|
||||||
.set_active(this.subscription().unwrap().muted());
|
.set_active(this.subscription().unwrap().muted());
|
||||||
|
self.retention_hours_spin_row
|
||||||
|
.set_value(this.subscription().unwrap().retention_hours() as f64);
|
||||||
|
let adj = self.retention_hours_spin_row.adjustment();
|
||||||
|
adj.set_upper(8760.0);
|
||||||
|
adj.set_step_increment(1.0);
|
||||||
|
adj.set_page_increment(24.0);
|
||||||
|
|
||||||
let debouncer = crate::async_utils::Debouncer::new();
|
let debouncer = crate::async_utils::Debouncer::new();
|
||||||
self.display_name_entry.connect_changed({
|
self.display_name_entry.connect_changed({
|
||||||
@ -64,6 +72,14 @@ mod imp {
|
|||||||
this.update_muted(switch);
|
this.update_muted(switch);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let this = self.obj().clone();
|
||||||
|
self.retention_hours_spin_row
|
||||||
|
.adjustment()
|
||||||
|
.connect_value_changed({
|
||||||
|
move |adj| {
|
||||||
|
this.update_retention_hours(adj.value() as u32);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl WidgetImpl for SubscriptionInfoDialog {}
|
impl WidgetImpl for SubscriptionInfoDialog {}
|
||||||
@ -99,4 +115,11 @@ impl SubscriptionInfoDialog {
|
|||||||
.spawn(async move { sub.set_muted(switch.is_active()).await })
|
.spawn(async move { sub.set_muted(switch.is_active()).await })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn update_retention_hours(&self, value: u32) {
|
||||||
|
if let Some(sub) = self.subscription() {
|
||||||
|
let sub = sub.clone();
|
||||||
|
self.error_boundary()
|
||||||
|
.spawn(async move { sub.set_retention_hours(value).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user