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
+232
View File
@@ -0,0 +1,232 @@
//! Chats widget: the list of open 1:1 conversations.
use serde_json::{json, Value};
use super::{chat, contact_view, notif, P};
use crate::{app::Ctx, db, handler::Origin, rpc, utils};
const FILTERS: &[&str] = &["all", "roster"];
pub fn build(ctx: &Ctx) -> String {
let filter = ctx.st(|st| st.chats_filter.clone()).unwrap_or_else(|| "all".into());
ctx.render(
"Chats/chats.html",
json!({
"filters": FILTERS,
"filter": filter,
"addplaceholder": ctx.t("chats.empty", &["<i class=\"material-symbols\">chat_add_on</i>"]),
}),
)
}
fn item_id(jid: &str) -> String {
format!("#{}", utils::cleanup_id(&format!("{}_chat_item", utils::slugify(jid))))
}
pub async fn call(ctx: &Ctx, func: &str, p: &P<'_>) -> bool {
match func {
"ajaxHttpGet" => {
rpc!(ctx, "MovimTpl.fill", "#chats", prepare_chats(ctx).await);
rpc!(ctx, "Chats.refresh");
}
"ajaxGetMAMHistory" => {
let jid = p.s(0);
let ctx = ctx.to_ws();
tokio::spawn(async move {
match jid {
Some(jid) => {
crate::mam::fetch_history(&ctx, &jid, false).await;
}
None => crate::mam::catch_up(&ctx).await,
}
});
}
"ajaxSetFilter" => {
let filter = p.str(0);
if FILTERS.contains(&filter.as_str()) {
ctx.st(|st| st.chats_filter = filter.clone());
}
rpc!(ctx, "Chats.refreshFilters");
}
"ajaxOpen" => {
let and_show = p.b(1);
let history = p.v(2).is_none() || p.b(2);
open(ctx, &p.str(0), and_show, history).await;
}
"ajaxClose" => close(ctx, &p.str(0), p.b(1)).await,
_ => return false,
}
true
}
pub async fn prepare_chats(ctx: &Ctx) -> String {
let me = ctx.jid();
let chats = db::open_chats(ctx.db(), &me).await.unwrap_or_default();
let mut html = String::new();
for jid in chats.iter().rev() {
if utils::validate_jid(jid) && *jid != me {
html.push_str(&prepare_chat(ctx, jid).await);
}
}
html
}
pub async fn prepare_chat(ctx: &Ctx, jid: &str) -> String {
let me = ctx.jid();
let (bare, resource) = match jid.split_once('/') {
Some((b, r)) => (b.to_string(), Some(r.to_string())),
None => (jid.to_string(), None),
};
let contact = contact_view(ctx, jid).await;
let count = db::unreads_count(ctx.db(), &me, jid, false).await.unwrap_or(0);
let last = db::last_message(ctx.db(), &me, jid, false).await.ok().flatten();
let composing = ctx.st(|st| st.chat_states.get(jid).is_some_and(|s| !s.is_empty())).unwrap_or(false);
let message = match last {
Some(m) => {
let file = db::message_file(ctx.db(), m.mid).await.ok().flatten().map(|f| {
let t = f.type_.clone().unwrap_or_default();
json!({
"isPicture": utils::type_is_picture(&t),
"isAudio": utils::type_is_audio(&t),
"isVideo": utils::type_is_video(&t),
"thumbnail_type": f.thumbnail_type,
"thumbnail_url": f.thumbnail_url,
})
});
let body_html = m
.body
.as_deref()
.map(|b| ctx.emoji(&utils::html_escape(&b.replace('\n', " "))).html)
.unwrap_or_default();
json!({
"date": ctx.prepare_date(&m.published, true, true),
"date_full": ctx.prepare_date(&m.published, false, true),
"mine": m.jidfrom == me,
"displayed": m.displayed,
"delivered": m.delivered,
"encrypted": m.encrypted,
"retracted": m.retracted,
"type": m.type_,
"file": file,
"body": m.body,
"body_html": body_html,
})
}
None => Value::Null,
};
ctx.render(
"Chats/_chats_item.html",
json!({
"contact": contact,
"count": count,
"message": message,
"composing": composing,
"resource": resource,
"bare": bare,
"in_call": super::visio::is_in_call(ctx, jid),
}),
)
}
/// The discussion was opened: refresh its item
pub async fn chat_open(ctx: &Ctx, jid: &str) {
if !utils::validate_jid(jid) {
return;
}
let me = ctx.jid();
if !db::open_chat_exists(ctx.db(), &me, jid).await.unwrap_or(false) {
let _ = db::open_chat_touch(ctx.db(), &me, jid, None).await;
rpc!(ctx, "MovimTpl.prepend", "#chats", prepare_chat(ctx, jid).await);
} else {
rpc!(ctx, "MovimTpl.replace", item_id(jid), prepare_chat(ctx, jid).await);
}
rpc!(ctx, "MovimUtils.addClass", item_id(jid), "active");
rpc!(ctx, "Chats.refresh");
}
pub async fn refresh_item(ctx: &Ctx, jid: &str) {
let me = ctx.jid();
if db::open_chat_exists(ctx.db(), &me, jid).await.unwrap_or(false) {
rpc!(ctx, "MovimTpl.replace", item_id(jid), prepare_chat(ctx, jid).await);
rpc!(ctx, "Chats.refresh");
if notif::is_current(ctx, jid) {
rpc!(ctx, "MovimUtils.addClass", item_id(jid), "active");
}
}
}
pub async fn open(ctx: &Ctx, jid: &str, and_show: bool, history: bool) {
let me = ctx.jid();
if !utils::validate_jid(jid) || jid == me {
return;
}
if history {
let ctx2 = ctx.to_ws();
let jid2 = jid.to_string();
tokio::spawn(async move {
crate::mam::fetch_history(&ctx2, &jid2, false).await;
});
}
let _ = db::open_chat_touch(ctx.db(), &me, jid, None).await;
rpc!(ctx, "MovimTpl.remove", item_id(jid));
rpc!(ctx, "MovimTpl.prepend", "#chats", prepare_chat(ctx, jid).await);
rpc!(ctx, "Chats.refresh");
if notif::is_current(ctx, jid) || and_show {
rpc!(ctx, "Chats.setActive", jid);
}
if and_show {
rpc!(ctx, "Chat.get", jid);
}
}
pub async fn close(ctx: &Ctx, jid: &str, close_discussion: bool) {
if !utils::validate_jid(jid) {
return;
}
let me = ctx.jid();
let _ = db::open_chat_close(ctx.db(), &me, jid).await;
let _ = db::messages_mark_seen(ctx.db(), &me, jid).await;
rpc!(ctx, "MovimTpl.remove", item_id(jid));
rpc!(ctx, "Chats.refresh");
notif::on_chat_counter(ctx).await;
if close_discussion {
rpc!(ctx, "Chat_ajaxGet");
}
notif::clear(ctx, &format!("chat|{jid}")).await;
}
pub async fn on_message(ctx: &Ctx, m: &db::Message, origin: &Origin) {
if m.is_muc() {
return;
}
let me = ctx.jid();
let jid = if m.jidfrom == me { m.jidto.clone() } else { m.jidfrom.clone() };
if matches!(origin, Origin::Mam { .. }) && m.seen {
// History sync: only refresh an already displayed item
refresh_item(ctx, &jid).await;
return;
}
let _ = db::open_chat_touch(ctx.db(), &me, &jid, Some(&m.published)).await;
rpc!(ctx, "MovimTpl.remove", item_id(&jid));
rpc!(ctx, "MovimTpl.prepend", "#chats", prepare_chat(ctx, &jid).await);
rpc!(ctx, "Chats.refresh");
if notif::is_current(ctx, &jid) {
rpc!(ctx, "Chats.setActive", jid.clone());
}
if !m.seen {
notif::on_chat_counter(ctx).await;
}
let _ = chat::message_preview;
}
pub async fn on_presence(ctx: &Ctx, jid: &str) {
refresh_item(ctx, jid).await;
}
pub async fn on_chat_state(ctx: &Ctx, jid: &str) {
let composing = ctx.st(|st| st.chat_states.get(jid).is_some_and(|s| !s.is_empty())).unwrap_or(false);
let f = if composing { "MovimUtils.addClass" } else { "MovimUtils.removeClass" };
rpc!(ctx, f, format!("{} span.primary", item_id(jid)), "composing");
}