diff --git a/public/s/js/messages.js b/public/s/js/messages.js
index 1bc6f99..b1824b4 100644
--- a/public/s/js/messages.js
+++ b/public/s/js/messages.js
@@ -395,34 +395,60 @@ if (window.__dmLoaded) {
// ── Online presence ───────────────────────────────────────────────────
if (window._dmPresenceTicker) clearInterval(window._dmPresenceTicker);
+ if (window._dmPresenceHandler) {
+ document.removeEventListener('f0ck:global_chat_presence', window._dmPresenceHandler);
+ }
const presenceEl = document.getElementById('dm-presence');
+
+ const renderPresence = (online, lastSeenUnix) => {
+ if (!presenceEl) return;
+ const now = ~~(Date.now() / 1000);
+ const diff = now - (lastSeenUnix || 0);
+
+ if (online) {
+ presenceEl.className = 'dm-presence dm-presence--online';
+ presenceEl.innerHTML = 'Online';
+ } else if (diff < 3600) {
+ const mins = Math.max(1, Math.floor(diff / 60));
+ presenceEl.className = 'dm-presence dm-presence--recent';
+ presenceEl.innerHTML = `Active ${mins}m ago`;
+ } else {
+ presenceEl.className = 'dm-presence dm-presence--offline';
+ presenceEl.innerHTML = lastSeenUnix
+ ? `Last seen ${timeAgo(new Date(lastSeenUnix * 1000).toISOString())}`
+ : 'Last seen a long time ago';
+ }
+ };
+
const pollPresence = async () => {
if (!presenceEl) return;
try {
const data = await (await fetch(`/api/dm/presence/${currentOtherId}`)).json();
- if (!data.success) { presenceEl.innerHTML = ''; return; }
-
- const now = ~~(Date.now() / 1000);
- const diff = now - (data.last_seen || 0); // seconds ago
-
- if (data.online) {
- presenceEl.className = 'dm-presence dm-presence--online';
- presenceEl.innerHTML = 'Online';
- } else if (diff < 3600) {
- // Active within the last hour
- const mins = Math.max(1, Math.floor(diff / 60));
- presenceEl.className = 'dm-presence dm-presence--recent';
- presenceEl.innerHTML = `Active ${mins}m ago`;
- } else {
- presenceEl.className = 'dm-presence dm-presence--offline';
- presenceEl.innerHTML = data.last_seen
- ? `Last seen ${timeAgo(new Date(data.last_seen * 1000).toISOString())}`
- : 'Last seen a long time ago';
- }
- } catch { if (presenceEl) presenceEl.innerHTML = ''; }
+ if (data.success) renderPresence(data.online, data.last_seen);
+ } catch { /* non-critical */ }
};
+ // Live update: fires immediately when the other user connects/disconnects from SSE
+ window._dmPresenceHandler = (e) => {
+ const users = e.detail?.users || [];
+ const isLive = users.some(u => u.id === currentOtherId);
+ if (isLive) {
+ // They just connected — mark online immediately, no HTTP round-trip needed
+ renderPresence(true, ~~(Date.now() / 1000));
+ } else {
+ // They disconnected — fall back to a fresh poll to get accurate last_seen
+ pollPresence();
+ }
+ };
+ document.addEventListener('f0ck:global_chat_presence', window._dmPresenceHandler);
+
+ // Also poll on page-visibility restore (user returns to tab)
+ const onVisible = () => { if (!document.hidden) pollPresence(); };
+ document.removeEventListener('visibilitychange', window._dmVisibilityHandler || (() => {}));
+ window._dmVisibilityHandler = onVisible;
+ document.addEventListener('visibilitychange', onVisible);
+
await pollPresence();
window._dmPresenceTicker = setInterval(pollPresence, 30_000);
}
diff --git a/src/inc/routes/notifications.mjs b/src/inc/routes/notifications.mjs
index 601ae2c..3c0d274 100644
--- a/src/inc/routes/notifications.mjs
+++ b/src/inc/routes/notifications.mjs
@@ -16,6 +16,7 @@ function broadcastChatPresence() {
if (!seen.has(client.userId)) {
seen.add(client.userId);
users.push({
+ id: client.userId,
username: client.username,
display_name: client.display_name,
avatar_file: client.avatar_file,