clear messages
This commit is contained in:
+32
-14
@@ -7,11 +7,12 @@
|
||||
#
|
||||
# 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),
|
||||
# RUST_IMAGE (default rust:1-$DEBIAN), DOCKER (default docker; podman works).
|
||||
# Overridable: DEBIAN (default trixie: Debian 13 and newer; bookworm for 12),
|
||||
# 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
|
||||
|
||||
DEBIAN=${DEBIAN:-bookworm}
|
||||
DEBIAN=${DEBIAN:-trixie}
|
||||
RUST_IMAGE=${RUST_IMAGE:-rust:1-$DEBIAN}
|
||||
DOCKER=${DOCKER:-docker}
|
||||
|
||||
@@ -19,8 +20,6 @@ die() { echo "error: $*" >&2; exit 1; }
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
[ -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)
|
||||
name="movim-$version-$DEBIAN"
|
||||
@@ -29,20 +28,39 @@ name="movim-$version-$DEBIAN"
|
||||
cache="target/docker-$DEBIAN"
|
||||
mkdir -p "$cache/cargo"
|
||||
|
||||
echo "==> Building $name in $RUST_IMAGE"
|
||||
"$DOCKER" run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
-v "$PWD:/src" -w /src \
|
||||
-e CARGO_HOME="/src/$cache/cargo" \
|
||||
-e CARGO_TARGET_DIR="/src/$cache" \
|
||||
"$RUST_IMAGE" \
|
||||
sh -c 'rustc --version && cargo build --release --locked'
|
||||
bin="$cache/release/movim"
|
||||
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)" \
|
||||
-v "$PWD:/src" -w /src \
|
||||
-e CARGO_HOME="/src/$cache/cargo" \
|
||||
-e CARGO_TARGET_DIR="/src/$cache" \
|
||||
"$RUST_IMAGE" \
|
||||
sh -c 'rustc --version && cargo build --release --locked'
|
||||
fi
|
||||
|
||||
echo "==> Packing dist/$name.tar.gz"
|
||||
stage=$(mktemp -d)
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
mkdir "$stage/$name"
|
||||
cp "$cache/release/movim" install.sh "$stage/$name/"
|
||||
cp "$bin" install.sh "$stage/$name/"
|
||||
cp -R frontend templates "$stage/$name/"
|
||||
mkdir -p dist
|
||||
tar -C "$stage" -czf "dist/$name.tar.gz" "$name"
|
||||
|
||||
@@ -21,3 +21,7 @@ this_device = This device
|
||||
|
||||
[omemo]
|
||||
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
@@ -9,8 +9,8 @@
|
||||
#
|
||||
# Overridable: MOVIM_USER (default movim, created if missing), PREFIX
|
||||
# (default /opt/movim), DATA_DIR (default /var/lib/movim), CONF_DIR
|
||||
# (default /etc/movim), XMPP_DOMAIN and WEB_HOST (asked when run in a
|
||||
# terminal and the configuration does not exist yet).
|
||||
# (default /etc/movim), XMPP_DOMAIN, WEB_HOST and MOVIM_PORT (asked when run
|
||||
# in a terminal and the configuration does not exist yet).
|
||||
set -eu
|
||||
|
||||
MOVIM_USER=${MOVIM_USER:-movim}
|
||||
@@ -19,19 +19,47 @@ DATA_DIR=${DATA_DIR:-/var/lib/movim}
|
||||
CONF_DIR=${CONF_DIR:-/etc/movim}
|
||||
XMPP_DOMAIN=${XMPP_DOMAIN:-}
|
||||
WEB_HOST=${WEB_HOST:-}
|
||||
MOVIM_PORT=${MOVIM_PORT:-}
|
||||
UNIT=/etc/systemd/system/movim.service
|
||||
MIN_RUSTC=1.94
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
|
||||
# Validators print why a value is refused.
|
||||
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() {
|
||||
check=${4:-valid_host}
|
||||
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
|
||||
echo " no terminal, $1 defaults to $3 (set $1=... to choose)"
|
||||
eval "$1=\$3"
|
||||
@@ -41,11 +69,10 @@ ask() {
|
||||
printf '%s [%s]: ' "$2" "$3"
|
||||
read -r answer || die "aborted"
|
||||
answer=${answer:-$3}
|
||||
if valid_host "$answer"; then
|
||||
if "$check" "$answer"; then
|
||||
eval "$1=\$answer"
|
||||
return 0
|
||||
fi
|
||||
echo " not a valid domain name: $answer"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -71,6 +98,7 @@ fi
|
||||
# --- Questions (before the build, so nobody has to wait for it) -----------
|
||||
if [ ! -e "$CONF_DIR/movim.env" ]; then
|
||||
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
|
||||
if [ -d /etc/nginx/sites-available ] && [ ! -e /etc/nginx/sites-available/movim ]; then
|
||||
default_web=movim.${XMPP_DOMAIN:-example.org}
|
||||
@@ -147,7 +175,7 @@ if [ ! -e "$CONF_DIR/movim.env" ]; then
|
||||
MOVIM_ROOT=$PREFIX
|
||||
MOVIM_DATA_DIR=$DATA_DIR
|
||||
# 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.
|
||||
MOVIM_DOMAIN=$XMPP_DOMAIN
|
||||
# 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
|
||||
echo " writing $nginx_site"
|
||||
sudo tee "$nginx_site" >/dev/null <<EOF
|
||||
# Movim behind nginx. Get the certificate first, e.g.
|
||||
# sudo certbot certonly --nginx -d $WEB_HOST
|
||||
# or point ssl_certificate* to your own.
|
||||
# Movim behind nginx. Add your HTTPS settings (certificates, headers) below.
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
@@ -192,8 +218,11 @@ server {
|
||||
http2 on;
|
||||
server_name $WEB_HOST;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/$WEB_HOST/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/$WEB_HOST/privkey.pem;
|
||||
# TODO: your HTTPS settings, e.g. include snippets/ssl.conf;
|
||||
# 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.
|
||||
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 " 2. sudo systemctl enable --now movim"
|
||||
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 nginx -t && sudo systemctl reload nginx"
|
||||
else
|
||||
|
||||
@@ -124,6 +124,13 @@ CREATE TABLE IF NOT EXISTS mam_earliests (
|
||||
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 (
|
||||
user_id 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?)
|
||||
}
|
||||
|
||||
pub async fn messages_clear(db: &Db, user: &str, jid: &str) -> Result<()> {
|
||||
sqlx::query("DELETE FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)")
|
||||
/// Clear the local history of a conversation. The archive is marked as fully
|
||||
/// 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(jid)
|
||||
.bind(jid)
|
||||
.execute(db)
|
||||
.bind(archive_key)
|
||||
.execute(&mut *tx)
|
||||
.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(())
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let types = if muc { MESSAGE_TYPE_MUC } else { MESSAGE_TYPE };
|
||||
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 open_chats 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 encrypted_passwords 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?;
|
||||
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
@@ -46,6 +46,9 @@ async fn query(ctx: &Ctx, q: MamQuery<'_>) -> Option<Page> {
|
||||
pub async fn catch_up(ctx: &Ctx) {
|
||||
let me = ctx.jid();
|
||||
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 {
|
||||
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) {
|
||||
let me = ctx.jid();
|
||||
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;
|
||||
return;
|
||||
};
|
||||
|
||||
+33
-2
@@ -70,8 +70,29 @@ pub async fn call(ctx: &Ctx, func: &str, p: &P<'_>) -> bool {
|
||||
"ajaxClearHistoryConfirm" => {
|
||||
let jid = p.str(0);
|
||||
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 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 {
|
||||
get_room(ctx, &jid, true).await;
|
||||
} 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 {
|
||||
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 {
|
||||
let conference = rooms::conference_view(ctx, jid).await;
|
||||
ctx.render(
|
||||
"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 {
|
||||
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,
|
||||
"in_call": in_call,
|
||||
"omemo": omemo,
|
||||
"cleared": cleared,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use minidom::Element;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
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 {
|
||||
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 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(
|
||||
"ContactActions/_contactactions_drawer.html",
|
||||
@@ -57,6 +58,7 @@ pub async fn get_drawer(ctx: &Ctx, jid: &str) {
|
||||
"call_audio": audio,
|
||||
"call_video": video,
|
||||
"omemo": omemo,
|
||||
"cleared": cleared,
|
||||
}),
|
||||
);
|
||||
super::drawer(ctx, "contact_drawer", &html, false, false);
|
||||
|
||||
@@ -494,10 +494,10 @@ pub async fn join(ctx: &Ctx, room: &str, nick: Option<&str>) {
|
||||
// History comes from MAM
|
||||
proto::muc_join(room, &nick, None, Some(0))
|
||||
} else {
|
||||
let since = db::latest_muc_message_date(ctx.db(), &ctx.jid(), room)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
let latest = db::latest_muc_message_date(ctx.db(), &ctx.jid(), room).await.ok().flatten();
|
||||
let cleared = db::history_cleared_at(ctx.db(), &ctx.jid(), &format!("muc:{room}")).await.ok().flatten();
|
||||
let since = latest
|
||||
.max(cleared)
|
||||
.and_then(|d| crate::dates::parse(&d))
|
||||
.map(|d| d.to_rfc3339_opts(chrono::SecondsFormat::Secs, true));
|
||||
match since {
|
||||
|
||||
@@ -142,6 +142,7 @@ pub static WIDGETS: &[WidgetDef] = &[
|
||||
("ajaxDisplayed", &["jid", "id"]),
|
||||
("ajaxClearHistory", &["jid"]),
|
||||
("ajaxClearHistoryConfirm", &["jid"]),
|
||||
("ajaxRestoreHistory", &["jid"]),
|
||||
("ajaxHttpGetPresences", &["room"]),
|
||||
("ajaxClearAndGetMessages", &["jid", "muc"]),
|
||||
("ajaxHttpGetExplore", &["page"]),
|
||||
|
||||
@@ -173,6 +173,16 @@
|
||||
<p class="line">{{ __('chat.clear_history') }}</p>
|
||||
</div>
|
||||
</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 }}');">
|
||||
<span class="primary icon gray">
|
||||
@@ -272,6 +282,16 @@
|
||||
<p class="line">{{ __('chat.clear_history') }}</p>
|
||||
</div>
|
||||
</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 />
|
||||
{% if contact.blocked %}
|
||||
<li onclick="ChatActions_ajaxUnblock('{{ contact.id|echapJS }}')">
|
||||
|
||||
@@ -133,6 +133,16 @@
|
||||
<p class="line">{{ __('chat.clear_history') }}</p>
|
||||
</div>
|
||||
</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>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user