197 lines
6.4 KiB
Rust
197 lines
6.4 KiB
Rust
//! 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 }));
|
|
}
|