|
|
|
@@ -1,29 +1,36 @@
|
|
|
|
|
// frontend/js/filechat.js — Chat intégré par fichier (#169)
|
|
|
|
|
// Panneau latéral : historique + saisie. Les messages entrants arrivent par
|
|
|
|
|
// SSE (event `chat_message` relayé depuis sync.js) ; si le panneau est fermé
|
|
|
|
|
// ou affiche un autre fichier, un toast notifie l'utilisateur.
|
|
|
|
|
// frontend/js/filechat.js — Chat fichier (#169) + chat général sidebar (#190)
|
|
|
|
|
//
|
|
|
|
|
// Deux surfaces, un module :
|
|
|
|
|
// 1. Panneau latéral du document (#169) — historique + saisie, suivi du
|
|
|
|
|
// document ouvert (followFileChat le re-cible à chaque renderFile).
|
|
|
|
|
// 2. Onglet « Chat » de la sidebar (#190) — conversation générale, pièces
|
|
|
|
|
// jointes (image/vidéo/URL), indicateur de messages non lus, recherche
|
|
|
|
|
// via la barre de filtre de la sidebar.
|
|
|
|
|
//
|
|
|
|
|
// Les deux reçoivent les messages en direct par SSE (`chat_message`, relayé
|
|
|
|
|
// depuis sync.js) : vault `__global__` = chat général, sinon chat fichier.
|
|
|
|
|
import { api, AuthManager } from "./auth.js";
|
|
|
|
|
import { t } from "./i18n.js";
|
|
|
|
|
import { showToast } from "./ui.js";
|
|
|
|
|
import { escapeHtml } from "./utils.js";
|
|
|
|
|
|
|
|
|
|
const PANEL_ID = "file-chat-panel";
|
|
|
|
|
const GLOBAL_VAULT = "__global__";
|
|
|
|
|
const UNREAD_KEY = "obsigate-chat-unread"; // {general: ts}
|
|
|
|
|
|
|
|
|
|
let _vault = null;
|
|
|
|
|
let _path = null;
|
|
|
|
|
let _title = "";
|
|
|
|
|
let _sending = false;
|
|
|
|
|
|
|
|
|
|
function _panel() {
|
|
|
|
|
let el = document.getElementById(PANEL_ID);
|
|
|
|
|
if (!el) {
|
|
|
|
|
el = document.createElement("aside");
|
|
|
|
|
el.id = PANEL_ID;
|
|
|
|
|
el.className = "file-chat-panel hidden";
|
|
|
|
|
el.setAttribute("aria-label", t("chat.title"));
|
|
|
|
|
document.body.appendChild(el);
|
|
|
|
|
}
|
|
|
|
|
return el;
|
|
|
|
|
}
|
|
|
|
|
// État du chat général (onglet sidebar)
|
|
|
|
|
let _generalMessages = [];
|
|
|
|
|
let _generalLoaded = false;
|
|
|
|
|
let _pendingAttach = null;
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Partagé
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function _esc(s) {
|
|
|
|
|
return escapeHtml(String(s == null ? "" : s));
|
|
|
|
|
}
|
|
|
|
@@ -38,6 +45,71 @@ function _formatTime(ts) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _currentUser() {
|
|
|
|
|
const u = AuthManager.getUser();
|
|
|
|
|
return (u && u.username) || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// URL dans le texte → lien cliquable (partage d'URL #190), texte sinon.
|
|
|
|
|
function _fillBody(body, text) {
|
|
|
|
|
const parts = String(text || "").split(/(https?:\/\/[^\s<>"']+)/g);
|
|
|
|
|
parts.forEach((part) => {
|
|
|
|
|
if (!part) return;
|
|
|
|
|
if (/^https?:\/\//.test(part)) {
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = part;
|
|
|
|
|
a.textContent = part;
|
|
|
|
|
a.target = "_blank";
|
|
|
|
|
a.rel = "noopener noreferrer";
|
|
|
|
|
a.className = "file-chat-link";
|
|
|
|
|
body.appendChild(a);
|
|
|
|
|
} else {
|
|
|
|
|
body.appendChild(document.createTextNode(part));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _attachEl(att) {
|
|
|
|
|
if (!att || !att.url) return null;
|
|
|
|
|
if (att.kind === "image") {
|
|
|
|
|
const img = document.createElement("img");
|
|
|
|
|
img.src = att.url;
|
|
|
|
|
img.alt = att.name || "image";
|
|
|
|
|
img.loading = "lazy";
|
|
|
|
|
img.className = "file-chat-image";
|
|
|
|
|
return img;
|
|
|
|
|
}
|
|
|
|
|
if (att.kind === "video") {
|
|
|
|
|
const v = document.createElement("video");
|
|
|
|
|
v.src = att.url;
|
|
|
|
|
v.controls = true;
|
|
|
|
|
v.preload = "metadata";
|
|
|
|
|
v.className = "file-chat-video";
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function _messageEl(m, currentUser) {
|
|
|
|
|
const mine = currentUser && m.user === currentUser;
|
|
|
|
|
const wrap = document.createElement("div");
|
|
|
|
|
wrap.className = "file-chat-msg" + (mine ? " mine" : "");
|
|
|
|
|
wrap.dataset.id = m.id || "";
|
|
|
|
|
const meta = document.createElement("div");
|
|
|
|
|
meta.className = "file-chat-meta";
|
|
|
|
|
meta.innerHTML =
|
|
|
|
|
`<span class="file-chat-user">${_esc(m.user)}</span>` +
|
|
|
|
|
`<span class="file-chat-ts">${_esc(_formatTime(m.ts))}</span>`;
|
|
|
|
|
const body = document.createElement("div");
|
|
|
|
|
body.className = "file-chat-body";
|
|
|
|
|
_fillBody(body, m.text);
|
|
|
|
|
wrap.appendChild(meta);
|
|
|
|
|
if (m.text) wrap.appendChild(body);
|
|
|
|
|
const att = _attachEl(m.attachment);
|
|
|
|
|
if (att) wrap.appendChild(att);
|
|
|
|
|
return wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renderChatMessages(container, messages, currentUser) {
|
|
|
|
|
container.innerHTML = "";
|
|
|
|
|
if (!messages || messages.length === 0) {
|
|
|
|
@@ -51,22 +123,29 @@ export function renderChatMessages(container, messages, currentUser) {
|
|
|
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _messageEl(m, currentUser) {
|
|
|
|
|
const mine = currentUser && m.user === currentUser;
|
|
|
|
|
const wrap = document.createElement("div");
|
|
|
|
|
wrap.className = "file-chat-msg" + (mine ? " mine" : "");
|
|
|
|
|
wrap.dataset.id = m.id || "";
|
|
|
|
|
const meta = document.createElement("div");
|
|
|
|
|
meta.className = "file-chat-meta";
|
|
|
|
|
meta.innerHTML =
|
|
|
|
|
`<span class="file-chat-user">${_esc(m.user)}</span>` +
|
|
|
|
|
`<span class="file-chat-ts">${_esc(_formatTime(m.ts))}</span>`;
|
|
|
|
|
const body = document.createElement("div");
|
|
|
|
|
body.className = "file-chat-body";
|
|
|
|
|
body.textContent = m.text || "";
|
|
|
|
|
wrap.appendChild(meta);
|
|
|
|
|
wrap.appendChild(body);
|
|
|
|
|
return wrap;
|
|
|
|
|
function _appendMessage(list, m) {
|
|
|
|
|
const empty = list.querySelector(".file-chat-empty");
|
|
|
|
|
if (empty) empty.remove();
|
|
|
|
|
const id = m.id;
|
|
|
|
|
if (id && list.querySelector(`[data-id="${CSS.escape(id)}"]`)) return false;
|
|
|
|
|
list.appendChild(_messageEl(m, _currentUser()));
|
|
|
|
|
list.scrollTop = list.scrollHeight;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 1. Panneau du document (#169)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function _panel() {
|
|
|
|
|
let el = document.getElementById(PANEL_ID);
|
|
|
|
|
if (!el) {
|
|
|
|
|
el = document.createElement("aside");
|
|
|
|
|
el.id = PANEL_ID;
|
|
|
|
|
el.className = "file-chat-panel hidden";
|
|
|
|
|
el.setAttribute("aria-label", t("chat.title"));
|
|
|
|
|
document.body.appendChild(el);
|
|
|
|
|
}
|
|
|
|
|
return el;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function _load() {
|
|
|
|
@@ -86,17 +165,21 @@ async function _load() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _currentUser() {
|
|
|
|
|
const u = AuthManager.getUser();
|
|
|
|
|
return (u && u.username) || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _renderShell(panel) {
|
|
|
|
|
panel.innerHTML = "";
|
|
|
|
|
const header = document.createElement("div");
|
|
|
|
|
header.className = "file-chat-header";
|
|
|
|
|
const titles = document.createElement("div");
|
|
|
|
|
titles.className = "file-chat-titles";
|
|
|
|
|
const title = document.createElement("h3");
|
|
|
|
|
title.textContent = t("chat.title");
|
|
|
|
|
titles.appendChild(title);
|
|
|
|
|
// #190 A — le document ciblé est affiché dans l'en-tête
|
|
|
|
|
const target = document.createElement("span");
|
|
|
|
|
target.className = "file-chat-target";
|
|
|
|
|
target.textContent = _title || _path || "";
|
|
|
|
|
target.title = _path || "";
|
|
|
|
|
titles.appendChild(target);
|
|
|
|
|
const close = document.createElement("button");
|
|
|
|
|
close.type = "button";
|
|
|
|
|
close.className = "file-chat-close";
|
|
|
|
@@ -104,7 +187,7 @@ function _renderShell(panel) {
|
|
|
|
|
close.setAttribute("aria-label", t("chat.close"));
|
|
|
|
|
close.textContent = "✕";
|
|
|
|
|
close.addEventListener("click", closeFileChat);
|
|
|
|
|
header.appendChild(title);
|
|
|
|
|
header.appendChild(titles);
|
|
|
|
|
header.appendChild(close);
|
|
|
|
|
|
|
|
|
|
const list = document.createElement("div");
|
|
|
|
@@ -134,9 +217,10 @@ function _renderShell(panel) {
|
|
|
|
|
panel.appendChild(form);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function openFileChat(vault, path) {
|
|
|
|
|
export async function openFileChat(vault, path, title) {
|
|
|
|
|
_vault = vault;
|
|
|
|
|
_path = path;
|
|
|
|
|
_title = title || path || "";
|
|
|
|
|
const panel = _panel();
|
|
|
|
|
_renderShell(panel);
|
|
|
|
|
panel.classList.remove("hidden");
|
|
|
|
@@ -150,13 +234,21 @@ export function closeFileChat() {
|
|
|
|
|
if (panel) panel.classList.add("hidden");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function toggleFileChat(vault, path) {
|
|
|
|
|
export function toggleFileChat(vault, path, title) {
|
|
|
|
|
const panel = document.getElementById(PANEL_ID);
|
|
|
|
|
if (panel && !panel.classList.contains("hidden") && _vault === vault && _path === path) {
|
|
|
|
|
closeFileChat();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
openFileChat(vault, path);
|
|
|
|
|
openFileChat(vault, path, title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// #190 A — appelé à chaque renderFile : le panneau ouvert suit le document.
|
|
|
|
|
export function followFileChat(vault, path, title) {
|
|
|
|
|
const panel = document.getElementById(PANEL_ID);
|
|
|
|
|
if (!panel || panel.classList.contains("hidden")) return;
|
|
|
|
|
if (_vault === vault && _path === path) return;
|
|
|
|
|
openFileChat(vault, path, title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function _send(input) {
|
|
|
|
@@ -170,11 +262,7 @@ async function _send(input) {
|
|
|
|
|
body: JSON.stringify({ path: _path, text }),
|
|
|
|
|
});
|
|
|
|
|
input.value = "";
|
|
|
|
|
const list = _panel().querySelector(".file-chat-list");
|
|
|
|
|
const empty = list.querySelector(".file-chat-empty");
|
|
|
|
|
if (empty) empty.remove();
|
|
|
|
|
list.appendChild(_messageEl(data.message, _currentUser()));
|
|
|
|
|
list.scrollTop = list.scrollHeight;
|
|
|
|
|
_appendMessage(_panel().querySelector(".file-chat-list"), data.message);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
showToast(t("chat.send_error"), "error");
|
|
|
|
|
} finally {
|
|
|
|
@@ -183,24 +271,216 @@ async function _send(input) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- SSE relay (appelé par sync.js sur l'événement `chat_message`) ---------
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 2. Chat général, onglet sidebar (#190 B)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function _sidebarList() {
|
|
|
|
|
return document.getElementById("sidebar-panel-chat-list");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Charge (ou recharge) l'historique du chat général dans la sidebar. */
|
|
|
|
|
export async function openSidebarChat() {
|
|
|
|
|
const list = _sidebarList();
|
|
|
|
|
if (!list) return;
|
|
|
|
|
try {
|
|
|
|
|
const data = await api("/api/chat");
|
|
|
|
|
_generalMessages = (data.messages || []).slice();
|
|
|
|
|
_generalLoaded = true;
|
|
|
|
|
renderChatMessages(list, _generalMessages, _currentUser());
|
|
|
|
|
_clearUnread();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
list.innerHTML = "";
|
|
|
|
|
const err = document.createElement("div");
|
|
|
|
|
err.className = "file-chat-empty";
|
|
|
|
|
err.textContent = t("chat.load_error");
|
|
|
|
|
list.appendChild(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Recherche dans le chat général (barre de filtre sidebar) — #190 C. */
|
|
|
|
|
export function filterChatMessages(query) {
|
|
|
|
|
const list = _sidebarList();
|
|
|
|
|
if (!list || !_generalLoaded) return;
|
|
|
|
|
const q = String(query || "").toLowerCase().trim();
|
|
|
|
|
const shown = q
|
|
|
|
|
? _generalMessages.filter((m) =>
|
|
|
|
|
((m.text || "") + " " + (m.user || "")).toLowerCase().includes(q),
|
|
|
|
|
)
|
|
|
|
|
: _generalMessages;
|
|
|
|
|
renderChatMessages(list, shown, _currentUser());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _sendInput() {
|
|
|
|
|
return document.getElementById("sidebar-panel-chat-input");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _renderPending() {
|
|
|
|
|
const holder = document.getElementById("sidebar-panel-chat-pending");
|
|
|
|
|
if (!holder) return;
|
|
|
|
|
holder.innerHTML = "";
|
|
|
|
|
if (!_pendingAttach) return;
|
|
|
|
|
const chip = document.createElement("span");
|
|
|
|
|
chip.className = "file-chat-pending";
|
|
|
|
|
chip.textContent =
|
|
|
|
|
(_pendingAttach.kind === "video" ? "🎬 " : "🖼 ") + (_pendingAttach.name || "pièce jointe");
|
|
|
|
|
const x = document.createElement("button");
|
|
|
|
|
x.type = "button";
|
|
|
|
|
x.textContent = "✕";
|
|
|
|
|
x.setAttribute("aria-label", t("chat.attach_remove"));
|
|
|
|
|
x.addEventListener("click", () => {
|
|
|
|
|
_pendingAttach = null;
|
|
|
|
|
_renderPending();
|
|
|
|
|
});
|
|
|
|
|
chip.appendChild(x);
|
|
|
|
|
holder.appendChild(chip);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Envoi d'un message du chat général (texte + pièce jointe éventuelle). */
|
|
|
|
|
export async function sendGeneralMessage() {
|
|
|
|
|
const input = _sendInput();
|
|
|
|
|
if (!input) return;
|
|
|
|
|
const text = (input.value || "").trim();
|
|
|
|
|
if (!text && !_pendingAttach) return;
|
|
|
|
|
const body = { text };
|
|
|
|
|
if (_pendingAttach) body.attachment = _pendingAttach;
|
|
|
|
|
try {
|
|
|
|
|
const data = await api("/api/chat", { method: "POST", body: JSON.stringify(body) });
|
|
|
|
|
input.value = "";
|
|
|
|
|
_pendingAttach = null;
|
|
|
|
|
_renderPending();
|
|
|
|
|
const list = _sidebarList();
|
|
|
|
|
if (list) {
|
|
|
|
|
_generalMessages.push(data.message);
|
|
|
|
|
_appendMessage(list, data.message);
|
|
|
|
|
}
|
|
|
|
|
input.focus();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
showToast(t("chat.send_error"), "error");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Sélecteur de pièce jointe (image/vidéo) → upload → envoi libre. */
|
|
|
|
|
export async function pickAttachment() {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const fileInput = document.createElement("input");
|
|
|
|
|
fileInput.type = "file";
|
|
|
|
|
fileInput.accept = "image/*,video/*";
|
|
|
|
|
fileInput.style.display = "none";
|
|
|
|
|
document.body.appendChild(fileInput);
|
|
|
|
|
fileInput.addEventListener("change", async () => {
|
|
|
|
|
const file = fileInput.files && fileInput.files[0];
|
|
|
|
|
fileInput.remove();
|
|
|
|
|
if (!file) return resolve(null);
|
|
|
|
|
try {
|
|
|
|
|
const form = new FormData();
|
|
|
|
|
form.append("file", file);
|
|
|
|
|
const res = await api("/api/chat/upload", { method: "POST", body: form });
|
|
|
|
|
_pendingAttach = res.attachment;
|
|
|
|
|
_renderPending();
|
|
|
|
|
resolve(_pendingAttach);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
showToast(t("chat.attach_error"), "error");
|
|
|
|
|
resolve(null);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
fileInput.click();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Messages non lus (#190 B) --------------------------------------------
|
|
|
|
|
function _readUnread() {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(localStorage.getItem(UNREAD_KEY) || "{}") || {};
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _clearUnread() {
|
|
|
|
|
const u = _readUnread();
|
|
|
|
|
u.general = Math.floor(Date.now() / 1000);
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(UNREAD_KEY, JSON.stringify(u));
|
|
|
|
|
} catch (_) { /* quota */ }
|
|
|
|
|
_renderBadge(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _renderBadge(count) {
|
|
|
|
|
const tab = document.getElementById("sidebar-tab-chat");
|
|
|
|
|
if (!tab) return;
|
|
|
|
|
let badge = tab.querySelector(".chat-unread-badge");
|
|
|
|
|
if (!count) {
|
|
|
|
|
if (badge) badge.remove();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!badge) {
|
|
|
|
|
badge = document.createElement("span");
|
|
|
|
|
badge.className = "chat-unread-badge";
|
|
|
|
|
tab.appendChild(badge);
|
|
|
|
|
}
|
|
|
|
|
badge.textContent = count > 99 ? "99+" : String(count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Compte les messages reçus depuis la dernière visite. */
|
|
|
|
|
export function refreshUnreadBadge(messages) {
|
|
|
|
|
const list = messages || _generalMessages;
|
|
|
|
|
if (!list.length) return _renderBadge(0);
|
|
|
|
|
const seen = _readUnread().general || 0;
|
|
|
|
|
const count = list.filter((m) => (m.ts || 0) > seen && m.user !== _currentUser()).length;
|
|
|
|
|
_renderBadge(count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Initialisation : formulaire + raccourcis clavier. */
|
|
|
|
|
export function initSidebarChat() {
|
|
|
|
|
const form = document.getElementById("sidebar-panel-chat-form");
|
|
|
|
|
if (!form) return;
|
|
|
|
|
const input = document.getElementById("sidebar-panel-chat-input");
|
|
|
|
|
form.addEventListener("submit", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
sendGeneralMessage();
|
|
|
|
|
});
|
|
|
|
|
const attach = document.getElementById("sidebar-panel-chat-attach");
|
|
|
|
|
if (attach) attach.addEventListener("click", () => pickAttachment());
|
|
|
|
|
if (input) {
|
|
|
|
|
input.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
sendGeneralMessage();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// SSE relay (appelé par sync.js sur l'événement `chat_message`)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
export function onChatMessage(data) {
|
|
|
|
|
if (!data || !data.message) return;
|
|
|
|
|
const isGeneral = data.vault === GLOBAL_VAULT;
|
|
|
|
|
|
|
|
|
|
if (isGeneral) {
|
|
|
|
|
_generalMessages.push(data.message);
|
|
|
|
|
const list = _sidebarList();
|
|
|
|
|
const panel = document.getElementById("sidebar-panel-chat");
|
|
|
|
|
const visible = panel && panel.classList.contains("active");
|
|
|
|
|
if (list && visible) {
|
|
|
|
|
_appendMessage(list, data.message);
|
|
|
|
|
_clearUnread();
|
|
|
|
|
} else if (data.message.user !== _currentUser()) {
|
|
|
|
|
refreshUnreadBadge(_generalMessages);
|
|
|
|
|
showToast(t("chat.new_message", { user: data.message.user }), "info");
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Chat d'un document (#169)
|
|
|
|
|
const panel = document.getElementById(PANEL_ID);
|
|
|
|
|
const isOpen =
|
|
|
|
|
panel && !panel.classList.contains("hidden") &&
|
|
|
|
|
_vault === data.vault && _path === data.path;
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
const list = panel.querySelector(".file-chat-list");
|
|
|
|
|
if (list) {
|
|
|
|
|
const empty = list.querySelector(".file-chat-empty");
|
|
|
|
|
if (empty) empty.remove();
|
|
|
|
|
// Évite le doublon si l'envoi local a déjà rendu ce message
|
|
|
|
|
const id = data.message.id;
|
|
|
|
|
if (id && list.querySelector(`[data-id="${CSS.escape(id)}"]`)) return;
|
|
|
|
|
list.appendChild(_messageEl(data.message, _currentUser()));
|
|
|
|
|
list.scrollTop = list.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
if (list) _appendMessage(list, data.message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (data.message.user === _currentUser()) return; // son propre message
|
|
|
|
|