240 lines
7.9 KiB
Rust
240 lines
7.9 KiB
Rust
//! 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))))
|
|
}
|
|
|
|
/// Put the item of a discussion on top of the list. The html is prepared before
|
|
/// touching the list: with an await between the remove and the prepend, two
|
|
/// concurrent calls (a message during a reload) left two items for one contact.
|
|
async fn put_on_top(ctx: &Ctx, jid: &str) {
|
|
let html = prepare_chat(ctx, jid).await;
|
|
rpc!(ctx, "MovimTpl.remove", item_id(jid));
|
|
rpc!(ctx, "MovimTpl.prepend", "#chats", html);
|
|
}
|
|
|
|
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) {
|
|
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;
|
|
put_on_top(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) {
|
|
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;
|
|
put_on_top(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;
|
|
put_on_top(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");
|
|
}
|