This commit is contained in:
nnduc
2026-10-24 14:03:52 +07:00
commit 4fe046de58
4790 changed files with 311146 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
//! Notif widget: notification counters, desktop notifications and snackbars.
//! `notifs_key` tracks what the user is currently looking at (`chat|jid`…).
use serde_json::{json, Value};
use super::P;
use crate::{app::Ctx, db, rpc};
pub struct Notification<'a> {
pub key: String,
pub title: String,
pub body: String,
pub url: String,
pub picture: Option<String>,
pub time: i64,
pub actions: Value,
pub data: Value,
pub rpc_call: Option<&'a str>,
}
/// Number of conversations with unread messages
pub async fn unreads(ctx: &Ctx) -> i64 {
let me = ctx.jid();
let rooms: Vec<String> = ctx
.st(|st| st.conferences.values().filter(|c| c.notify > 0).map(|c| c.jid.clone()).collect())
.unwrap_or_default();
db::unreads_total(ctx.db(), &me, &rooms).await.unwrap_or(0)
}
pub async fn append(ctx: &Ctx, n: Notification<'_>) {
let picture = n.picture.clone().unwrap_or_else(|| format!("{}theme/img/app/128.png", ctx.base_uri()));
let mut data = n.data.clone();
data["url"] = json!(n.url);
crate::push::notify(ctx, &n.key, &n.title, &n.body, &picture, &data).await;
rpc!(
ctx,
"Notif.desktop",
n.key.clone(),
chrono::Utc::now().timestamp(),
n.title.clone(),
n.body.clone(),
picture.clone(),
n.actions.clone(),
data
);
let first = n.key.split('|').next().unwrap_or("").to_string();
let current = ctx.st(|st| st.notifs_key.clone()).flatten();
let (first_count, key_count) = ctx
.st(|st| {
let c = st.notifs.shift_remove(&first).unwrap_or(0) + 1;
st.notifs.insert(first.clone(), c);
let mut k = 0;
if first != n.key {
k = st.notifs.shift_remove(&n.key).unwrap_or(0) + 1;
st.notifs.insert(n.key.clone(), k);
}
(c, k)
})
.unwrap_or((0, 0));
// Do not notify what is currently displayed
if current.as_deref() == Some(n.key.as_str()) {
return;
}
if first == "chat" {
rpc!(ctx, "Notif.counter", first.clone(), unreads(ctx).await);
} else {
rpc!(ctx, "Notif.counter", first.clone(), first_count);
}
if let Some(call) = n.rpc_call {
rpc!(ctx, call);
}
if first != n.key {
rpc!(ctx, "Notif.counter", n.key.clone(), key_count);
}
if !n.title.is_empty() {
let html = ctx.render(
"Notif/_notif.html",
json!({ "title": n.title, "body": n.body, "picture": picture, "url": n.url }),
);
rpc!(ctx, "Notif.snackbar", html, n.time);
}
super::presence::refresh_shortcuts(ctx).await;
}
pub async fn clear(ctx: &Ctx, key: &str) {
let removed = ctx
.st(|st| {
let counter = st.notifs.shift_remove(key)?;
let first = key.split('|').next().unwrap_or("").to_string();
let mut first_left = None;
if let Some(v) = st.notifs.get_mut(&first) {
*v -= counter;
first_left = Some((first.clone(), (*v).max(0)));
if *v <= 0 {
st.notifs.shift_remove(&first);
}
}
Some(first_left)
})
.flatten();
if let Some(first_left) = removed {
rpc!(ctx, "Notif.counter", key, 0);
rpc!(ctx, "Notif.clear", key);
if let Some((first, left)) = first_left {
rpc!(ctx, "Notif.counter", first, left);
}
if let Some(jid) = key.strip_prefix("chat|") {
rpc!(ctx, "Shortcuts.clear", jid);
}
}
}
pub fn current(ctx: &Ctx) -> Option<String> {
ctx.st(|st| st.notifs_key.clone()).flatten()
}
/// Is the conversation currently displayed and focused?
pub fn is_current(ctx: &Ctx, jid: &str) -> bool {
current(ctx).is_some_and(|k| k == format!("chat|{jid}"))
}
pub async fn call(ctx: &Ctx, func: &str, p: &P<'_>) -> bool {
match func {
"ajaxClear" => clear(ctx, &p.str(0)).await,
"ajaxGet" => {
let chat = p.v(0).is_none() || p.b(0);
let mut notifs: serde_json::Map<String, Value> = ctx
.st(|st| st.notifs.iter().map(|(k, v)| (k.clone(), json!(v))).collect())
.unwrap_or_default();
if chat {
notifs.insert("chat".into(), json!(unreads(ctx).await));
}
rpc!(ctx, "Notif.refresh", Value::Object(notifs));
}
"ajaxCurrent" => {
let key = p.str(0);
if key.contains('|') {
clear(ctx, &key).await;
}
let was_blurred = ctx.st(|st| st.notifs_key.as_deref() == Some("blurred")).unwrap_or(false);
ctx.st(|st| st.notifs_key = Some(key.clone()));
if was_blurred {
if let Some(jid) = key.strip_prefix("chat|") {
super::chat::on_focus_back(ctx, jid).await;
}
}
}
"ajaxRegisterPushSubscrition" => {
let (endpoint, auth, p256dh) = (p.str(0), p.str(1), p.str(2));
if endpoint.starts_with("https://") {
let _ = db::push_subscription_save(ctx.db(), &ctx.jid(), &endpoint, &p256dh, &auth).await;
}
}
"ajaxHttpTouchPushSubscription" => {
let _ = db::push_subscription_touch(ctx.db(), &p.str(0)).await;
}
"ajaxHttpRequest" => {
super::dialog(ctx, &ctx.render("Notif/_notif_request.html", json!({})), false, false);
}
"ajaxRequestGranted" => {
rpc!(
ctx,
"Notif.desktop",
chrono::Utc::now().timestamp(),
ctx.t("notification.request_title", &[]),
ctx.t("notification.request_granted", &[]),
Value::Null,
Value::Null,
Value::Null,
Value::Null,
Value::Null,
true
);
}
"ajaxRequestDenied" => ctx.toast(&ctx.t("notification.request_denied", &[])),
_ => return false,
}
true
}
pub async fn on_chat_counter(ctx: &Ctx) {
let n = unreads(ctx).await;
rpc!(ctx, "Notif.counter", "chat", n);
rpc!(ctx, "MovimUtils.setDataItem", "#chatcounter", "counter", n);
rpc!(ctx, "MovimUtils.setDataItem", "#chatheadercounter", "counter", n);
rpc!(ctx, "BottomNavigation.setChatNotification", json!({ "chat": n }));
}