From 9c129b7a378b0dfae151f56fc17dd5582eebbf4d Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Mon, 18 May 2026 17:36:07 +0200 Subject: [PATCH] add decent replying to direct messages --- public/s/css/f0ckm.css | 79 ++++++++++++++++++++++++++++--- public/s/js/messages.js | 100 +++++++++++++++++++++++++++++++++------- 2 files changed, 155 insertions(+), 24 deletions(-) diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 6dfbb05..d033d33 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -7667,7 +7667,7 @@ video.autoplay-gif { opacity: 0; pointer-events: none; transition: opacity 0.2s ease-in-out, background 0.2s; - box-shadow: 0 2px 4px rgba(0,0,0,0.5); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); } .image-embed-wrap:hover .admin-delete-attachment-btn, @@ -11006,7 +11006,7 @@ body.layout-modern .tag-controls { } .dm-msg-mine .dm-bubble { - background: var(--badge-bg); + background: #090909; color: var(--white); border-bottom-right-radius: 4px; } @@ -11064,7 +11064,7 @@ body.layout-modern .tag-controls { .dm-msg-action-btn:hover { color: var(--fg, #ddd); - background: rgba(255,255,255,0.07); + background: rgba(255, 255, 255, 0.07); } .dm-msg-action-btn[data-action="delete"]:hover { @@ -11079,8 +11079,8 @@ body.layout-modern .tag-controls { .dm-edit-textarea { width: 100%; min-height: 60px; - background: rgba(255,255,255,0.05); - border: 1px solid rgba(255,255,255,0.15); + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; color: var(--fg, #ddd); font: inherit; @@ -11111,7 +11111,7 @@ body.layout-modern .tag-controls { } .dm-edit-cancel { - background: rgba(255,255,255,0.08); + background: rgba(255, 255, 255, 0.08); color: var(--fg, #ddd); } @@ -11124,7 +11124,14 @@ body.layout-modern .tag-controls { /* Delete fade-out */ @keyframes dm-msg-fade-out { - to { opacity: 0; transform: scaleY(0); max-height: 0; margin: 0; padding: 0; overflow: hidden; } + to { + opacity: 0; + transform: scaleY(0); + max-height: 0; + margin: 0; + padding: 0; + overflow: hidden; + } } .dm-msg-deleting { @@ -11133,6 +11140,64 @@ body.layout-modern .tag-controls { overflow: hidden; } +/* ── Reply banner ────────────────────────────────────────── */ +.dm-reply-banner { + display: flex; + align-items: center; + gap: 7px; + background: rgba(255, 255, 255, 0.05); + border-left: 3px solid var(--badge-bg, #4a90d9); + border-radius: 4px; + padding: 5px 8px; + font-size: 0.8em; + color: #aaa; + margin-bottom: 5px; + animation: dm-reply-in 0.12s ease; +} + +@keyframes dm-reply-in { + from { + opacity: 0; + transform: translateY(-4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.dm-reply-banner__icon { + color: var(--badge-bg, #4a90d9); + flex-shrink: 0; +} + +.dm-reply-banner__body { + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.dm-reply-banner__body strong { + color: var(--fg, #ddd); +} + +.dm-reply-banner__close { + background: none; + border: none; + color: #555; + cursor: pointer; + padding: 2px 4px; + font-size: 1em; + flex-shrink: 0; + transition: color 0.12s; +} + +.dm-reply-banner__close:hover { + color: var(--fg, #ddd); +} + /* ── Send form ───────────────────────────────────────────── */ .dm-send-form { gap: 8px; diff --git a/public/s/js/messages.js b/public/s/js/messages.js index 98b5916..68ee6ff 100644 --- a/public/s/js/messages.js +++ b/public/s/js/messages.js @@ -305,6 +305,7 @@ if (window.__dmLoaded) { const renderedIds = new Set(); let threadMessages = []; // Cache for re-rendering (e.g. emojis) const dmPostPreviewCache = new Map(); // itemId → { item, meta } | null + let activeReply = null; // { senderName, preview } | null // Title management — global across all pages let _dmTitleCount = 0; @@ -718,21 +719,24 @@ if (window.__dmLoaded) { `
${content}
` + `${escHtml(time)}${editedBit}`; - if (isMine) { - const actions = document.createElement('div'); - actions.className = 'dm-msg-actions'; - actions.innerHTML = - `` + - ``; - actions.addEventListener('click', async (e) => { - const btn = e.target.closest('[data-action]'); - if (!btn) return; - const action = btn.dataset.action; - if (action === 'edit') editDmMessage(m, div); - if (action === 'delete') deleteDmMessage(m, div); - }); - div.appendChild(actions); - } + // Actions row — always shown (reply for all, edit/delete for own) + const actions = document.createElement('div'); + actions.className = 'dm-msg-actions'; + + const replyBtn = ``; + const editBtn = ``; + const delBtn = ``; + + actions.innerHTML = replyBtn + (isMine ? editBtn + delBtn : ''); + actions.addEventListener('click', async (e) => { + const btn = e.target.closest('[data-action]'); + if (!btn) return; + const action = btn.dataset.action; + if (action === 'reply') replyToDmMessage(m, div); + if (action === 'edit') editDmMessage(m, div); + if (action === 'delete') deleteDmMessage(m, div); + }); + div.appendChild(actions); // Async: extract post IDs from raw plaintext and inject preview cards into the bubble if (m.plaintext) resolvePostPreviews(div, m.plaintext); @@ -743,6 +747,62 @@ if (window.__dmLoaded) { return div; } + function replyToDmMessage(m, div) { + const thread = document.getElementById('dm-thread'); + const otherName = thread?.dataset.otherName || 'them'; + const myName = window.f0ckSession?.user || 'me'; + const senderName = (m.sender_id === myId) ? myName : otherName; + + const raw = m.plaintext || ''; + + // Strip attachment sentinels from the text part, collect their filenames + const attachments = parseAttachmentSentinels(raw); + const textOnly = raw.replace(/\[attachment:\d+:[A-Za-z0-9+/=]+:[^:]+:\d+\]/g, '').trim(); + const textPreview = textOnly.replace(/\n/g, ' ').slice(0, 80) + (textOnly.length > 80 ? '…' : ''); + + // One preview string for the banner; quoteLines drives the actual sent quote + const preview = [textPreview, ...attachments.map(a => a.filename)].filter(Boolean).join(' · '); + + // Build the multi-line blockquote: first line has the sender + text, then one line per attachment + const quoteLines = []; + quoteLines.push(`> @${senderName}: ${textPreview}`); + for (const att of attachments) quoteLines.push(`> ${att.filename}`); + + activeReply = { senderName, preview, quoteLines }; + showReplyBanner(); + + const input = document.getElementById('dm-input'); + if (input) { input.focus(); } + } + + function showReplyBanner() { + const form = document.getElementById('dm-send-form'); + if (!form) return; + + let banner = form.querySelector('.dm-reply-banner'); + if (!banner) { + banner = document.createElement('div'); + banner.className = 'dm-reply-banner'; + form.insertBefore(banner, form.firstChild); + } + + const name = escHtml(activeReply?.senderName || ''); + const preview = escHtml(activeReply?.preview || ''); + banner.innerHTML = + `` + + `${name} ${preview}` + + ``; + + banner.querySelector('.dm-reply-banner__close').addEventListener('click', clearReply); + } + + function clearReply() { + activeReply = null; + const banner = document.querySelector('.dm-reply-banner'); + if (banner) banner.remove(); + } + + async function editDmMessage(m, div) { if (div.dataset.editing === 'true') return; if (!m.plaintext) { showFlashMsg('Cannot edit — message could not be decrypted', 'error'); return; } @@ -1670,8 +1730,13 @@ if (window.__dmLoaded) { form.addEventListener('submit', async (e) => { e.preventDefault(); if (sendInFlight) return; // prevent double-submit - const text = input.value.trim(); - if (!text) return; + const rawText = input.value.trim(); + if (!rawText) return; + + // Prepend reply quote if active + const text = activeReply + ? `${activeReply.quoteLines.join('\n')}\n\n${rawText}` + : rawText; // If recipient has no key, block sending — we cannot encrypt for them if (!currentOtherPubKey) { @@ -1692,6 +1757,7 @@ if (window.__dmLoaded) { if (res.success) { input.value = ''; input.style.height = ''; + clearReply(); // Optimistic render with the real server ID so dedup works const thread = document.getElementById('dm-thread');