diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css
index d033d33..81ddda9 100644
--- a/public/s/css/f0ckm.css
+++ b/public/s/css/f0ckm.css
@@ -10845,6 +10845,53 @@ body.layout-modern .tag-controls {
text-decoration: underline;
}
+.dm-header-name-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ line-height: 1.2;
+}
+
+.dm-presence {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 0.72em;
+ color: #666;
+ min-height: 14px;
+}
+
+.dm-presence-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ background: #555;
+}
+
+.dm-presence--online .dm-presence-dot {
+ background: #3ddc84;
+ box-shadow: 0 0 0 2px rgba(61, 220, 132, 0.25);
+ animation: dm-presence-pulse 2s ease infinite;
+}
+
+.dm-presence--online {
+ color: #3ddc84;
+}
+
+.dm-presence--recent .dm-presence-dot {
+ background: #f5a623;
+}
+
+.dm-presence--recent {
+ color: #f5a623;
+}
+
+@keyframes dm-presence-pulse {
+ 0%, 100% { box-shadow: 0 0 0 2px rgba(61, 220, 132, 0.25); }
+ 50% { box-shadow: 0 0 0 4px rgba(61, 220, 132, 0.1); }
+}
+
/* ── Key notice banner ───────────────────────────────────── */
.dm-key-notice {
background: rgba(255, 200, 80, 0.15);
diff --git a/public/s/js/messages.js b/public/s/js/messages.js
index 9a6885d..1bc6f99 100644
--- a/public/s/js/messages.js
+++ b/public/s/js/messages.js
@@ -392,6 +392,39 @@ if (window.__dmLoaded) {
(el.textContent = timeAgo(el.dataset.ts)));
tickTimestamps(); // Run immediately so values are fresh
window._dmTimestampTicker = setInterval(tickTimestamps, 10_000);
+
+ // ── Online presence ───────────────────────────────────────────────────
+ if (window._dmPresenceTicker) clearInterval(window._dmPresenceTicker);
+
+ const presenceEl = document.getElementById('dm-presence');
+ 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 = ''; }
+ };
+
+ await pollPresence();
+ window._dmPresenceTicker = setInterval(pollPresence, 30_000);
}
async function loadThread(thread, prepend = false) {
diff --git a/src/inc/routes/messages.mjs b/src/inc/routes/messages.mjs
index f9bc199..f9d3771 100644
--- a/src/inc/routes/messages.mjs
+++ b/src/inc/routes/messages.mjs
@@ -406,6 +406,24 @@ export default (router, tpl) => {
}
});
+ // Presence check — last_seen timestamp for a given user (online = seen < 5 min ago)
+ router.get(/\/api\/dm\/presence\/(?\d+)/, async (req, res) => {
+ if (!getPrivateMessages()) return json(res, { success: false }, 404);
+ if (!req.session) return json(res, { success: false }, 401);
+ const userId = parseInt(req.params.userId, 10);
+ try {
+ const rows = await db`SELECT last_seen FROM "user" WHERE id = ${userId} AND banned = false LIMIT 1`;
+ if (!rows.length) return json(res, { success: false, msg: 'User not found' }, 404);
+ const lastSeen = rows[0].last_seen || 0; // unix seconds
+ const now = ~~(Date.now() / 1000);
+ const online = (now - lastSeen) < 300; // 5-minute window
+ return json(res, { success: true, online, last_seen: lastSeen });
+ } catch (err) {
+ console.error('[DM] presence failed:', err);
+ return json(res, { success: false }, 500);
+ }
+ });
+
// Total unread DM count (for navbar badge polling)
router.get('/api/dm/unread', async (req, res) => {
if (!getPrivateMessages()) return json(res, { success: true, count: 0 });
diff --git a/views/messages-conversation.html b/views/messages-conversation.html
index b8caeb3..315f9d2 100644
--- a/views/messages-conversation.html
+++ b/views/messages-conversation.html
@@ -13,7 +13,10 @@
@else
@endif
-
+