clear messages

This commit is contained in:
nnduc
2026-10-24 15:22:36 +07:00
parent 84e9594284
commit 5467b0aa2c
11 changed files with 260 additions and 42 deletions
+26 -8
View File
@@ -7,11 +7,12 @@
# #
# Copy it to the server, then: tar xzf movim-*.tar.gz && cd movim-* && ./install.sh # Copy it to the server, then: tar xzf movim-*.tar.gz && cd movim-* && ./install.sh
# #
# Overridable: DEBIAN (default bookworm: Debian 12, also runs on 13 trixie), # Overridable: DEBIAN (default trixie: Debian 13 and newer; bookworm for 12),
# RUST_IMAGE (default rust:1-$DEBIAN), DOCKER (default docker; podman works). # RUST_IMAGE (default rust:1-$DEBIAN), DOCKER (default docker; podman works),
# REBUILD (yes/no; asked when a binary exists, default yes without a terminal).
set -eu set -eu
DEBIAN=${DEBIAN:-bookworm} DEBIAN=${DEBIAN:-trixie}
RUST_IMAGE=${RUST_IMAGE:-rust:1-$DEBIAN} RUST_IMAGE=${RUST_IMAGE:-rust:1-$DEBIAN}
DOCKER=${DOCKER:-docker} DOCKER=${DOCKER:-docker}
@@ -19,8 +20,6 @@ die() { echo "error: $*" >&2; exit 1; }
cd "$(dirname "$0")" cd "$(dirname "$0")"
[ -f Cargo.toml ] || die "run this from the movim-rs repository" [ -f Cargo.toml ] || die "run this from the movim-rs repository"
command -v "$DOCKER" >/dev/null || die "$DOCKER not found"
"$DOCKER" info >/dev/null 2>&1 || die "cannot reach $DOCKER; start it (sudo systemctl start docker) and make sure you may use it (docker group)"
version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1) version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)
name="movim-$version-$DEBIAN" name="movim-$version-$DEBIAN"
@@ -29,20 +28,39 @@ name="movim-$version-$DEBIAN"
cache="target/docker-$DEBIAN" cache="target/docker-$DEBIAN"
mkdir -p "$cache/cargo" mkdir -p "$cache/cargo"
echo "==> Building $name in $RUST_IMAGE" bin="$cache/release/movim"
"$DOCKER" run --rm \ rebuild=${REBUILD:-}
if [ -z "$rebuild" ] && [ -x "$bin" ]; then
if [ -t 0 ]; then
printf 'A binary exists (built %s). Rebuild? [Y/n] ' "$(date -r "$bin" '+%Y-%m-%d %H:%M')"
read -r answer || answer=n
case $answer in [nN]*) rebuild=no ;; *) rebuild=yes ;; esac
else
rebuild=yes
fi
fi
if [ "$rebuild" = no ]; then
[ -x "$bin" ] || die "REBUILD=no but there is no binary at $bin"
echo "==> Reusing $bin"
else
command -v "$DOCKER" >/dev/null || die "$DOCKER not found"
"$DOCKER" info >/dev/null 2>&1 || die "cannot reach $DOCKER; start it (sudo systemctl start docker) and make sure you may use it (docker group)"
echo "==> Building $name in $RUST_IMAGE"
"$DOCKER" run --rm \
--user "$(id -u):$(id -g)" \ --user "$(id -u):$(id -g)" \
-v "$PWD:/src" -w /src \ -v "$PWD:/src" -w /src \
-e CARGO_HOME="/src/$cache/cargo" \ -e CARGO_HOME="/src/$cache/cargo" \
-e CARGO_TARGET_DIR="/src/$cache" \ -e CARGO_TARGET_DIR="/src/$cache" \
"$RUST_IMAGE" \ "$RUST_IMAGE" \
sh -c 'rustc --version && cargo build --release --locked' sh -c 'rustc --version && cargo build --release --locked'
fi
echo "==> Packing dist/$name.tar.gz" echo "==> Packing dist/$name.tar.gz"
stage=$(mktemp -d) stage=$(mktemp -d)
trap 'rm -rf "$stage"' EXIT trap 'rm -rf "$stage"' EXIT
mkdir "$stage/$name" mkdir "$stage/$name"
cp "$cache/release/movim" install.sh "$stage/$name/" cp "$bin" install.sh "$stage/$name/"
cp -R frontend templates "$stage/$name/" cp -R frontend templates "$stage/$name/"
mkdir -p dist mkdir -p dist
tar -C "$stage" -czf "dist/$name.tar.gz" "$name" tar -C "$stage" -czf "dist/$name.tar.gz" "$name"
+4
View File
@@ -21,3 +21,7 @@ this_device = This device
[omemo] [omemo]
this_device = This device this_device = This device
[chat]
restore_history = Restore the history from the server
history_restored = The history will be fetched again from the server
+43 -14
View File
@@ -9,8 +9,8 @@
# #
# Overridable: MOVIM_USER (default movim, created if missing), PREFIX # Overridable: MOVIM_USER (default movim, created if missing), PREFIX
# (default /opt/movim), DATA_DIR (default /var/lib/movim), CONF_DIR # (default /opt/movim), DATA_DIR (default /var/lib/movim), CONF_DIR
# (default /etc/movim), XMPP_DOMAIN and WEB_HOST (asked when run in a # (default /etc/movim), XMPP_DOMAIN, WEB_HOST and MOVIM_PORT (asked when run
# terminal and the configuration does not exist yet). # in a terminal and the configuration does not exist yet).
set -eu set -eu
MOVIM_USER=${MOVIM_USER:-movim} MOVIM_USER=${MOVIM_USER:-movim}
@@ -19,19 +19,47 @@ DATA_DIR=${DATA_DIR:-/var/lib/movim}
CONF_DIR=${CONF_DIR:-/etc/movim} CONF_DIR=${CONF_DIR:-/etc/movim}
XMPP_DOMAIN=${XMPP_DOMAIN:-} XMPP_DOMAIN=${XMPP_DOMAIN:-}
WEB_HOST=${WEB_HOST:-} WEB_HOST=${WEB_HOST:-}
MOVIM_PORT=${MOVIM_PORT:-}
UNIT=/etc/systemd/system/movim.service UNIT=/etc/systemd/system/movim.service
MIN_RUSTC=1.94 MIN_RUSTC=1.94
die() { echo "error: $*" >&2; exit 1; } die() { echo "error: $*" >&2; exit 1; }
# Validators print why a value is refused.
valid_host() { valid_host() {
printf '%s' "$1" | grep -Eq '^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$' printf '%s' "$1" | grep -Eq '^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$' && return 0
echo " not a valid domain name: $1"
return 1
} }
# ask VAR "question" default: prompt on the terminal until a valid hostname. port_in_use() {
command -v ss >/dev/null && [ -n "$(ss -Htln "sport = :$1")" ]
}
# The service has no capabilities, so no privileged ports.
valid_port() {
case $1 in ''|*[!0-9]*) echo " not a port number: $1"; return 1 ;; esac
if [ "$1" -lt 1024 ] || [ "$1" -gt 65535 ]; then
echo " port must be between 1024 and 65535"
return 1
fi
if port_in_use "$1"; then
echo " port $1 is already used by another service (ss -tlnp)"
return 1
fi
}
free_port() {
p=$1
while port_in_use "$p"; do p=$((p + 1)); done
echo "$p"
}
# ask VAR "question" default [validator]: prompt on the terminal until valid.
ask() { ask() {
check=${4:-valid_host}
eval "current=\${$1}" eval "current=\${$1}"
[ -z "$current" ] || { valid_host "$current" || die "$1=$current is not a valid domain name"; return 0; } [ -z "$current" ] || { "$check" "$current" >&2 || die "invalid $1"; return 0; }
if [ ! -t 0 ]; then if [ ! -t 0 ]; then
echo " no terminal, $1 defaults to $3 (set $1=... to choose)" echo " no terminal, $1 defaults to $3 (set $1=... to choose)"
eval "$1=\$3" eval "$1=\$3"
@@ -41,11 +69,10 @@ ask() {
printf '%s [%s]: ' "$2" "$3" printf '%s [%s]: ' "$2" "$3"
read -r answer || die "aborted" read -r answer || die "aborted"
answer=${answer:-$3} answer=${answer:-$3}
if valid_host "$answer"; then if "$check" "$answer"; then
eval "$1=\$answer" eval "$1=\$answer"
return 0 return 0
fi fi
echo " not a valid domain name: $answer"
done done
} }
@@ -71,6 +98,7 @@ fi
# --- Questions (before the build, so nobody has to wait for it) ----------- # --- Questions (before the build, so nobody has to wait for it) -----------
if [ ! -e "$CONF_DIR/movim.env" ]; then if [ ! -e "$CONF_DIR/movim.env" ]; then
ask XMPP_DOMAIN "XMPP domain users log in with (MOVIM_DOMAIN)" example.org ask XMPP_DOMAIN "XMPP domain users log in with (MOVIM_DOMAIN)" example.org
ask MOVIM_PORT "Local port for Movim (behind nginx)" "$(free_port 8080)" valid_port
fi fi
if [ -d /etc/nginx/sites-available ] && [ ! -e /etc/nginx/sites-available/movim ]; then if [ -d /etc/nginx/sites-available ] && [ ! -e /etc/nginx/sites-available/movim ]; then
default_web=movim.${XMPP_DOMAIN:-example.org} default_web=movim.${XMPP_DOMAIN:-example.org}
@@ -147,7 +175,7 @@ if [ ! -e "$CONF_DIR/movim.env" ]; then
MOVIM_ROOT=$PREFIX MOVIM_ROOT=$PREFIX
MOVIM_DATA_DIR=$DATA_DIR MOVIM_DATA_DIR=$DATA_DIR
# Listen on localhost only, behind a TLS reverse proxy (WebSocket on /ws/). # Listen on localhost only, behind a TLS reverse proxy (WebSocket on /ws/).
MOVIM_BIND=127.0.0.1:8080 MOVIM_BIND=127.0.0.1:$MOVIM_PORT
# XMPP domain proposed on the login page. # XMPP domain proposed on the login page.
MOVIM_DOMAIN=$XMPP_DOMAIN MOVIM_DOMAIN=$XMPP_DOMAIN
# Comma separated XMPP domains allowed to log in (empty: all). # Comma separated XMPP domains allowed to log in (empty: all).
@@ -176,9 +204,7 @@ if [ -d /etc/nginx/sites-available ]; then
if [ ! -e "$nginx_site" ]; then if [ ! -e "$nginx_site" ]; then
echo " writing $nginx_site" echo " writing $nginx_site"
sudo tee "$nginx_site" >/dev/null <<EOF sudo tee "$nginx_site" >/dev/null <<EOF
# Movim behind nginx. Get the certificate first, e.g. # Movim behind nginx. Add your HTTPS settings (certificates, headers) below.
# sudo certbot certonly --nginx -d $WEB_HOST
# or point ssl_certificate* to your own.
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
@@ -192,8 +218,11 @@ server {
http2 on; http2 on;
server_name $WEB_HOST; server_name $WEB_HOST;
ssl_certificate /etc/letsencrypt/live/$WEB_HOST/fullchain.pem; # TODO: your HTTPS settings, e.g. include snippets/ssl.conf;
ssl_certificate_key /etc/letsencrypt/live/$WEB_HOST/privkey.pem; # Movim already sends X-Content-Type-Options, X-Frame-Options and
# Referrer-Policy on pages; if your file adds them too, hide Movim's to
# avoid duplicates (proxy_hide_header X-Frame-Options; ... in both
# locations below).
# HTTP file uploads go through Movim, match your XMPP upload limit. # HTTP file uploads go through Movim, match your XMPP upload limit.
client_max_body_size 100m; client_max_body_size 100m;
@@ -274,7 +303,7 @@ echo "Installed. Not enabled or started; as admin:"
echo " 1. check $CONF_DIR/movim.env" echo " 1. check $CONF_DIR/movim.env"
echo " 2. sudo systemctl enable --now movim" echo " 2. sudo systemctl enable --now movim"
if [ -n "$nginx_site" ]; then if [ -n "$nginx_site" ]; then
echo " 3. get a certificate for the nginx site ($nginx_site), then:" echo " 3. add your HTTPS settings to $nginx_site (see the TODO), then:"
echo " sudo ln -s $nginx_site /etc/nginx/sites-enabled/movim" echo " sudo ln -s $nginx_site /etc/nginx/sites-enabled/movim"
echo " sudo nginx -t && sudo systemctl reload nginx" echo " sudo nginx -t && sudo systemctl reload nginx"
else else
+105 -6
View File
@@ -124,6 +124,13 @@ CREATE TABLE IF NOT EXISTS mam_earliests (
PRIMARY KEY (user_id, jid) PRIMARY KEY (user_id, jid)
); );
CREATE TABLE IF NOT EXISTS history_clears (
user_id TEXT NOT NULL,
jid TEXT NOT NULL,
cleared_at TEXT NOT NULL,
PRIMARY KEY (user_id, jid)
);
CREATE TABLE IF NOT EXISTS omemo_states ( CREATE TABLE IF NOT EXISTS omemo_states (
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
jid TEXT NOT NULL, jid TEXT NOT NULL,
@@ -694,17 +701,63 @@ pub async fn messages_count(db: &Db, user: &str, jid: &str) -> Result<i64> {
.await?) .await?)
} }
pub async fn messages_clear(db: &Db, user: &str, jid: &str) -> Result<()> { /// Clear the local history of a conversation. The archive is marked as fully
sqlx::query("DELETE FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)") /// known and the clear date is kept, so the cleared messages are not fetched
/// again from MAM or MUC history (`archive_key`: jid, or `muc:` + room).
pub async fn messages_clear(db: &Db, user: &str, jid: &str, archive_key: &str) -> Result<()> {
let mut tx = db.begin().await?;
for sql in [
"DELETE FROM reactions WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?))",
"DELETE FROM message_files WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?))",
"DELETE FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)",
] {
sqlx::query(sql).bind(user).bind(jid).bind(jid).execute(&mut *tx).await?;
}
sqlx::query("INSERT OR REPLACE INTO mam_earliests (user_id, jid, earliest) VALUES (?, ?, NULL)")
.bind(user) .bind(user)
.bind(jid) .bind(archive_key)
.bind(jid) .execute(&mut *tx)
.execute(db)
.await?; .await?;
sqlx::query("DELETE FROM mam_earliests WHERE user_id = ? AND jid = ?").bind(user).bind(jid).execute(db).await?; sqlx::query("INSERT OR REPLACE INTO history_clears (user_id, jid, cleared_at) VALUES (?, ?, ?)")
.bind(user)
.bind(archive_key)
.bind(crate::utils::now_sql())
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(()) Ok(())
} }
/// Undo `messages_clear`: the cleared messages can be fetched again from MAM
pub async fn history_restore(db: &Db, user: &str, archive_key: &str) -> Result<()> {
let mut tx = db.begin().await?;
for sql in [
"DELETE FROM history_clears WHERE user_id = ? AND jid = ?",
"DELETE FROM mam_earliests WHERE user_id = ? AND jid = ?",
] {
sqlx::query(sql).bind(user).bind(archive_key).execute(&mut *tx).await?;
}
tx.commit().await?;
Ok(())
}
/// When the history of a conversation was last cleared (`archive_key` as above)
pub async fn history_cleared_at(db: &Db, user: &str, archive_key: &str) -> Result<Option<String>> {
Ok(sqlx::query_scalar("SELECT cleared_at FROM history_clears WHERE user_id = ? AND jid = ?")
.bind(user)
.bind(archive_key)
.fetch_optional(db)
.await?)
}
/// Latest clear of a 1:1 conversation
pub async fn latest_chat_clear(db: &Db, user: &str) -> Result<Option<String>> {
Ok(sqlx::query_scalar("SELECT MAX(cleared_at) FROM history_clears WHERE user_id = ? AND jid NOT LIKE 'muc:%'")
.bind(user)
.fetch_one(db)
.await?)
}
pub async fn unreads_count(db: &Db, user: &str, jid: &str, muc: bool) -> Result<i64> { pub async fn unreads_count(db: &Db, user: &str, jid: &str, muc: bool) -> Result<i64> {
let types = if muc { MESSAGE_TYPE_MUC } else { MESSAGE_TYPE }; let types = if muc { MESSAGE_TYPE_MUC } else { MESSAGE_TYPE };
let types_sql = types.iter().map(|t| format!("'{t}'")).collect::<Vec<_>>().join(","); let types_sql = types.iter().map(|t| format!("'{t}'")).collect::<Vec<_>>().join(",");
@@ -1032,6 +1085,7 @@ pub async fn user_delete_data(db: &Db, user: &str) -> Result<()> {
"DELETE FROM messages WHERE user_id = ?", "DELETE FROM messages WHERE user_id = ?",
"DELETE FROM open_chats WHERE user_id = ?", "DELETE FROM open_chats WHERE user_id = ?",
"DELETE FROM mam_earliests WHERE user_id = ?", "DELETE FROM mam_earliests WHERE user_id = ?",
"DELETE FROM history_clears WHERE user_id = ?",
"DELETE FROM omemo_states WHERE user_id = ?", "DELETE FROM omemo_states WHERE user_id = ?",
"DELETE FROM encrypted_passwords WHERE user_id = ?", "DELETE FROM encrypted_passwords WHERE user_id = ?",
"DELETE FROM push_subscriptions WHERE user_id = ?", "DELETE FROM push_subscriptions WHERE user_id = ?",
@@ -1083,3 +1137,48 @@ pub async fn push_subscription_toggle(db: &Db, user: &str, id: i64, enabled: boo
.await?; .await?;
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn clear_history_is_remembered() {
// A file: every pool connection to sqlite::memory: gets its own database
let path = std::env::temp_dir().join(format!("movim-test-{}.sqlite", std::process::id()));
let db = connect(&format!("sqlite://{}?mode=rwc", path.display())).await.unwrap();
let (me, bob) = ("me@example.org", "bob@example.org");
let m = Message {
user_id: me.into(),
id: "m1".into(),
jidfrom: bob.into(),
jidto: me.into(),
type_: "chat".into(),
body: Some("hi".into()),
published: "2026-01-01 10:00:00".into(),
..Default::default()
};
let mid = message_save(&db, &m).await.unwrap();
reactions_replace(&db, mid, bob, &["👍".into()]).await.unwrap();
assert!(!mam_earliest_exists(&db, me, bob).await.unwrap());
messages_clear(&db, me, bob, bob).await.unwrap();
assert_eq!(messages_count(&db, me, bob).await.unwrap(), 0);
assert!(reactions_for(&db, &[mid]).await.unwrap().is_empty());
// No MAM refetch when the chat is opened again, catch-up starts after the clear
assert!(mam_earliest_exists(&db, me, bob).await.unwrap());
let cleared = history_cleared_at(&db, me, bob).await.unwrap().unwrap();
assert!(cleared.as_str() > "2026-01-01 10:00:00");
assert_eq!(latest_chat_clear(&db, me).await.unwrap(), Some(cleared));
assert_eq!(history_cleared_at(&db, me, "muc:room@conference.example.org").await.unwrap(), None);
history_restore(&db, me, bob).await.unwrap();
assert!(!mam_earliest_exists(&db, me, bob).await.unwrap());
assert_eq!(history_cleared_at(&db, me, bob).await.unwrap(), None);
db.close().await;
for ext in ["", "-wal", "-shm"] {
let _ = std::fs::remove_file(format!("{}{ext}", path.display()));
}
}
}
+5 -1
View File
@@ -46,6 +46,9 @@ async fn query(ctx: &Ctx, q: MamQuery<'_>) -> Option<Page> {
pub async fn catch_up(ctx: &Ctx) { pub async fn catch_up(ctx: &Ctx) {
let me = ctx.jid(); let me = ctx.jid();
let latest = db::latest_message_date(ctx.db(), &me).await.ok().flatten(); let latest = db::latest_message_date(ctx.db(), &me).await.ok().flatten();
// Never before a history clear: the cleared messages would come back.
let cleared = db::latest_chat_clear(ctx.db(), &me).await.ok().flatten();
let latest = latest.max(cleared);
match latest { match latest {
Some(latest) => { Some(latest) => {
@@ -154,7 +157,8 @@ pub async fn fetch_history(ctx: &Ctx, jid: &str, muc: bool) -> usize {
pub async fn catch_up_room(ctx: &Ctx, room: &str) { pub async fn catch_up_room(ctx: &Ctx, room: &str) {
let me = ctx.jid(); let me = ctx.jid();
let latest = db::latest_muc_message_date(ctx.db(), &me, room).await.ok().flatten(); let latest = db::latest_muc_message_date(ctx.db(), &me, room).await.ok().flatten();
let Some(latest) = latest else { let cleared = db::history_cleared_at(ctx.db(), &me, &format!("muc:{room}")).await.ok().flatten();
let Some(latest) = latest.max(cleared) else {
fetch_history(ctx, room, true).await; fetch_history(ctx, room, true).await;
return; return;
}; };
+33 -2
View File
@@ -70,8 +70,29 @@ pub async fn call(ctx: &Ctx, func: &str, p: &P<'_>) -> bool {
"ajaxClearHistoryConfirm" => { "ajaxClearHistoryConfirm" => {
let jid = p.str(0); let jid = p.str(0);
if utils::validate_jid(&jid) { if utils::validate_jid(&jid) {
let _ = db::messages_clear(ctx.db(), &ctx.jid(), &jid).await;
let muc = ctx.st(|st| st.conferences.contains_key(&jid)).unwrap_or(false); let muc = ctx.st(|st| st.conferences.contains_key(&jid)).unwrap_or(false);
let archive_key = if muc { format!("muc:{jid}") } else { jid.clone() };
if let Err(e) = db::messages_clear(ctx.db(), &ctx.jid(), &jid, &archive_key).await {
tracing::warn!("clear history of {jid}: {e}");
}
if muc {
get_room(ctx, &jid, true).await;
} else {
get(ctx, Some(&jid), false).await;
}
}
}
"ajaxRestoreHistory" => {
let jid = p.str(0);
if utils::validate_jid(&jid) {
let muc = ctx.st(|st| st.conferences.contains_key(&jid)).unwrap_or(false);
let archive_key = if muc { format!("muc:{jid}") } else { jid.clone() };
if let Err(e) = db::history_restore(ctx.db(), &ctx.jid(), &archive_key).await {
tracing::warn!("restore history of {jid}: {e}");
return true;
}
ctx.toast(&ctx.t("chat.history_restored", &[]));
// Reopening fetches from MAM, scrolling up fetches older pages
if muc { if muc {
get_room(ctx, &jid, true).await; get_room(ctx, &jid, true).await;
} else { } else {
@@ -105,11 +126,20 @@ pub async fn call(ctx: &Ctx, func: &str, p: &P<'_>) -> bool {
pub async fn prepare_header(ctx: &Ctx, jid: &str, muc: bool) -> String { pub async fn prepare_header(ctx: &Ctx, jid: &str, muc: bool) -> String {
let counter = notif::unreads(ctx).await; let counter = notif::unreads(ctx).await;
let archive_key = if muc { format!("muc:{jid}") } else { jid.to_string() };
let cleared = db::history_cleared_at(ctx.db(), &ctx.jid(), &archive_key).await.ok().flatten().is_some();
if muc { if muc {
let conference = rooms::conference_view(ctx, jid).await; let conference = rooms::conference_view(ctx, jid).await;
ctx.render( ctx.render(
"Chat/_chat_header.html", "Chat/_chat_header.html",
json!({ "jid": jid, "muc": true, "counter": counter, "conference": conference, "in_call": super::visio::has_call(ctx) }), json!({
"jid": jid,
"muc": true,
"counter": counter,
"conference": conference,
"in_call": super::visio::has_call(ctx),
"cleared": cleared,
}),
) )
} else { } else {
let contact = contact_view(ctx, jid).await; let contact = contact_view(ctx, jid).await;
@@ -129,6 +159,7 @@ pub async fn prepare_header(ctx: &Ctx, jid: &str, muc: bool) -> String {
"call_video": video, "call_video": video,
"in_call": in_call, "in_call": in_call,
"omemo": omemo, "omemo": omemo,
"cleared": cleared,
}), }),
) )
} }
+3 -1
View File
@@ -6,7 +6,7 @@ use minidom::Element;
use serde_json::{json, Value}; use serde_json::{json, Value};
use super::{contact_view, P}; use super::{contact_view, P};
use crate::{app::Ctx, proto, rpc, utils, xmpp}; use crate::{app::Ctx, db, proto, rpc, utils, xmpp};
pub fn fingerprint(b64: &str) -> String { pub fn fingerprint(b64: &str) -> String {
let Ok(bytes) = B64.decode(b64) else { return String::new() }; let Ok(bytes) = B64.decode(b64) else { return String::new() };
@@ -46,6 +46,7 @@ pub async fn get_drawer(ctx: &Ctx, jid: &str) {
let (audio, video) = super::visio::call_targets(ctx, jid); let (audio, video) = super::visio::call_targets(ctx, jid);
let omemo = ctx.me().is_some_and(|u| u.omemoenabled) && jid != me; let omemo = ctx.me().is_some_and(|u| u.omemoenabled) && jid != me;
let cleared = db::history_cleared_at(ctx.db(), &me, jid).await.ok().flatten().is_some();
let html = ctx.render( let html = ctx.render(
"ContactActions/_contactactions_drawer.html", "ContactActions/_contactactions_drawer.html",
@@ -57,6 +58,7 @@ pub async fn get_drawer(ctx: &Ctx, jid: &str) {
"call_audio": audio, "call_audio": audio,
"call_video": video, "call_video": video,
"omemo": omemo, "omemo": omemo,
"cleared": cleared,
}), }),
); );
super::drawer(ctx, "contact_drawer", &html, false, false); super::drawer(ctx, "contact_drawer", &html, false, false);
+4 -4
View File
@@ -494,10 +494,10 @@ pub async fn join(ctx: &Ctx, room: &str, nick: Option<&str>) {
// History comes from MAM // History comes from MAM
proto::muc_join(room, &nick, None, Some(0)) proto::muc_join(room, &nick, None, Some(0))
} else { } else {
let since = db::latest_muc_message_date(ctx.db(), &ctx.jid(), room) let latest = db::latest_muc_message_date(ctx.db(), &ctx.jid(), room).await.ok().flatten();
.await let cleared = db::history_cleared_at(ctx.db(), &ctx.jid(), &format!("muc:{room}")).await.ok().flatten();
.ok() let since = latest
.flatten() .max(cleared)
.and_then(|d| crate::dates::parse(&d)) .and_then(|d| crate::dates::parse(&d))
.map(|d| d.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)); .map(|d| d.to_rfc3339_opts(chrono::SecondsFormat::Secs, true));
match since { match since {
+1
View File
@@ -142,6 +142,7 @@ pub static WIDGETS: &[WidgetDef] = &[
("ajaxDisplayed", &["jid", "id"]), ("ajaxDisplayed", &["jid", "id"]),
("ajaxClearHistory", &["jid"]), ("ajaxClearHistory", &["jid"]),
("ajaxClearHistoryConfirm", &["jid"]), ("ajaxClearHistoryConfirm", &["jid"]),
("ajaxRestoreHistory", &["jid"]),
("ajaxHttpGetPresences", &["room"]), ("ajaxHttpGetPresences", &["room"]),
("ajaxClearAndGetMessages", &["jid", "muc"]), ("ajaxClearAndGetMessages", &["jid", "muc"]),
("ajaxHttpGetExplore", &["page"]), ("ajaxHttpGetExplore", &["page"]),
+20
View File
@@ -173,6 +173,16 @@
<p class="line">{{ __('chat.clear_history') }}</p> <p class="line">{{ __('chat.clear_history') }}</p>
</div> </div>
</li> </li>
{% if cleared %}
<li onclick="Chat_ajaxRestoreHistory('{{ jid|echapJS }}')">
<span class="primary icon gray">
<i class="material-symbols">history</i>
</span>
<div>
<p class="line">{{ __('chat.restore_history') }}</p>
</div>
</li>
{% endif %}
<li onclick="Rooms_ajaxExit('{{ jid|echapJS }}');"> <li onclick="Rooms_ajaxExit('{{ jid|echapJS }}');">
<span class="primary icon gray"> <span class="primary icon gray">
@@ -272,6 +282,16 @@
<p class="line">{{ __('chat.clear_history') }}</p> <p class="line">{{ __('chat.clear_history') }}</p>
</div> </div>
</li> </li>
{% if cleared %}
<li onclick="Chat_ajaxRestoreHistory('{{ contact.id|echapJS }}')">
<span class="primary icon gray">
<i class="material-symbols">history</i>
</span>
<div>
<p class="line">{{ __('chat.restore_history') }}</p>
</div>
</li>
{% endif %}
<hr /> <hr />
{% if contact.blocked %} {% if contact.blocked %}
<li onclick="ChatActions_ajaxUnblock('{{ contact.id|echapJS }}')"> <li onclick="ChatActions_ajaxUnblock('{{ contact.id|echapJS }}')">
@@ -133,6 +133,16 @@
<p class="line">{{ __('chat.clear_history') }}</p> <p class="line">{{ __('chat.clear_history') }}</p>
</div> </div>
</li> </li>
{% if cleared %}
<li onclick="Chat_ajaxRestoreHistory('{{ contact.id|echapJS }}')">
<span class="primary icon gray">
<i class="material-symbols">history</i>
</span>
<div>
<p class="line">{{ __('chat.restore_history') }}</p>
</div>
</li>
{% endif %}
</ul> </ul>
{% endif %} {% endif %}