[Add] Persistent notification counts

This commit is contained in:
Alois 2026-09-07 16:19:31 +02:00
commit d460cb0677
8 changed files with 260 additions and 19 deletions

View file

@ -658,6 +658,12 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
.unwrap_or(authored_at),
);
crate::util::chats_util::upsert_contact(&tx, storage_owner, &contact)?;
if !sent_by_self {
tx.execute(
"UPDATE contacts SET notifications = CASE WHEN notifications < 9223372036854775807 THEN notifications + 1 ELSE notifications END WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, external_user],
)?;
}
tx.commit()?;
Ok(msg_id)
})

View file

@ -2,7 +2,7 @@ use crate::storage_error::StorageError;
use crate::users::contact::Contact;
use crate::util::db;
use crate::util::sync::{self, EntityType, Operation};
use rusqlite::params;
use rusqlite::{OptionalExtension, params};
pub(crate) fn upsert_contact(
tx: &rusqlite::Transaction<'_>,
@ -11,8 +11,11 @@ pub(crate) fn upsert_contact(
) -> Result<(), StorageError> {
tx.execute(
r#"
INSERT INTO contacts (storage_owner, user_id, user_name, created_at, last_message_at)
VALUES (?1, ?2, ?3, ?4, ?5)
INSERT INTO contacts (
storage_owner, user_id, user_name, created_at, last_message_at,
notifications, notifications_read_at
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(storage_owner, user_id) DO UPDATE SET
user_name = COALESCE(excluded.user_name, contacts.user_name),
created_at = MIN(contacts.created_at, excluded.created_at),
@ -28,6 +31,8 @@ pub(crate) fn upsert_contact(
contact.user_name,
contact.created_at,
contact.last_message_at,
contact.notifications,
contact.notifications_read_at,
],
)?;
sync::record_event(
@ -69,7 +74,8 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Result<Option<Contact>, Sto
db::with_db(|conn| {
match conn.query_row(
r#"
SELECT user_id, user_name, created_at, last_message_at
SELECT user_id, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND user_id = ?2
LIMIT 1
@ -81,6 +87,8 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Result<Option<Contact>, Sto
user_name: r.get(1)?,
created_at: r.get(2)?,
last_message_at: r.get(3)?,
notifications: r.get(4)?,
notifications_read_at: r.get(5)?,
})
},
) {
@ -95,7 +103,8 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
db::with_db(|conn| {
let mut stmt = conn.prepare(
r#"
SELECT user_id, user_name, created_at, last_message_at
SELECT user_id, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1
ORDER BY
@ -111,6 +120,8 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
user_name: r.get(1)?,
created_at: r.get(2)?,
last_message_at: r.get(3)?,
notifications: r.get(4)?,
notifications_read_at: r.get(5)?,
})
})?;
@ -121,3 +132,68 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
Ok(out)
})
}
pub fn read_notifications(
storage_owner: i64,
user_id: i64,
through: i64,
) -> Result<Option<Contact>, StorageError> {
db::with_immediate_transaction(|tx| {
let Some(current_read_at) = tx
.query_row(
"SELECT notifications_read_at FROM contacts WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, user_id],
|row| row.get::<_, i64>(0),
)
.optional()?
else {
return Ok(None);
};
let read_at = current_read_at.max(through);
let notifications = tx.query_row(
r#"
SELECT COUNT(*)
FROM messages
WHERE storage_owner = ?1
AND external_user = ?2
AND sent_by_self = 0
AND deleted_by_external = 0
AND history_deleted = 0
AND COALESCE(destination_iota_received_at, stored_at, authored_at, message_time) > ?3
"#,
params![storage_owner, user_id, read_at],
|row| row.get::<_, i64>(0),
)?;
tx.execute(
"UPDATE contacts SET notifications = ?3, notifications_read_at = ?4 WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, user_id, notifications, read_at],
)?;
sync::record_event(
tx,
storage_owner,
EntityType::Contact,
user_id,
Operation::Upsert,
)?;
Ok(Some(tx.query_row(
r#"
SELECT user_id, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND user_id = ?2
"#,
params![storage_owner, user_id],
|row| {
Ok(Contact {
user_id: row.get(0)?,
user_name: row.get(1)?,
created_at: row.get(2)?,
last_message_at: row.get(3)?,
notifications: row.get(4)?,
notifications_read_at: row.get(5)?,
})
},
)?))
})
}

View file

@ -182,6 +182,8 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
user_name TEXT,
created_at INTEGER NOT NULL,
last_message_at INTEGER,
notifications INTEGER NOT NULL DEFAULT 0,
notifications_read_at INTEGER NOT NULL DEFAULT 0,
UNIQUE(storage_owner, user_id)
);
CREATE INDEX IF NOT EXISTS idx_contacts_owner
@ -858,6 +860,33 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
conn.pragma_update(None, "user_version", 24)?;
}
if current_version < 25 {
let contacts_exist: bool = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'contacts')",
[],
|row| row.get(0),
)?;
if contacts_exist {
add_table_column_if_missing(
conn,
"contacts",
"notifications",
"notifications INTEGER NOT NULL DEFAULT 0",
)?;
add_table_column_if_missing(
conn,
"contacts",
"notifications_read_at",
"notifications_read_at INTEGER NOT NULL DEFAULT 0",
)?;
conn.execute(
"UPDATE contacts SET notifications_read_at = COALESCE(last_message_at, 0) WHERE notifications_read_at = 0 AND notifications = 0",
[],
)?;
}
conn.pragma_update(None, "user_version", 25)?;
}
Ok(())
}
@ -927,7 +956,7 @@ mod tests {
run_migrations_on_connection(&conn)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 24);
assert_eq!(version, 25);
for column in ["height", "reply_to", "edited_count", "deleted_by_external"] {
let mut statement =
conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?;
@ -946,7 +975,7 @@ mod tests {
run_migrations_on_connection(&conn)?;
run_migrations_on_connection(&conn)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 24);
assert_eq!(version, 25);
for table in [
"sync_heads",
"sync_events",
@ -992,7 +1021,7 @@ mod tests {
run_migrations_on_connection(&conn)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(version, 24);
assert_eq!(version, 25);
for column in [
"id",
"user_id",
@ -1087,7 +1116,7 @@ mod tests {
)?;
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
assert_eq!(preserved, "remote_committed");
assert_eq!(version, 24);
assert_eq!(version, 25);
Ok(())
}
}

View file

@ -4,7 +4,7 @@ use crate::util::db;
use rusqlite::{OptionalExtension, Transaction, params};
use std::collections::BTreeMap;
pub const CACHE_SCHEMA_VERSION: i64 = 4;
pub const CACHE_SCHEMA_VERSION: i64 = 5;
pub const STALE_CLIENT_SYNC_STATE_MS: i64 = 90 * 24 * 60 * 60 * 1_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]