init
This commit is contained in:
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user