add decent replying to direct messages

This commit is contained in:
2026-05-18 17:36:07 +02:00
parent 0393878c9f
commit 9c129b7a37
2 changed files with 155 additions and 24 deletions

View File

@@ -7667,7 +7667,7 @@ video.autoplay-gif {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: opacity 0.2s ease-in-out, background 0.2s; 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, .image-embed-wrap:hover .admin-delete-attachment-btn,
@@ -11006,7 +11006,7 @@ body.layout-modern .tag-controls {
} }
.dm-msg-mine .dm-bubble { .dm-msg-mine .dm-bubble {
background: var(--badge-bg); background: #090909;
color: var(--white); color: var(--white);
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
@@ -11064,7 +11064,7 @@ body.layout-modern .tag-controls {
.dm-msg-action-btn:hover { .dm-msg-action-btn:hover {
color: var(--fg, #ddd); 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 { .dm-msg-action-btn[data-action="delete"]:hover {
@@ -11079,8 +11079,8 @@ body.layout-modern .tag-controls {
.dm-edit-textarea { .dm-edit-textarea {
width: 100%; width: 100%;
min-height: 60px; min-height: 60px;
background: rgba(255,255,255,0.05); background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255,255,255,0.15); border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px; border-radius: 6px;
color: var(--fg, #ddd); color: var(--fg, #ddd);
font: inherit; font: inherit;
@@ -11111,7 +11111,7 @@ body.layout-modern .tag-controls {
} }
.dm-edit-cancel { .dm-edit-cancel {
background: rgba(255,255,255,0.08); background: rgba(255, 255, 255, 0.08);
color: var(--fg, #ddd); color: var(--fg, #ddd);
} }
@@ -11124,7 +11124,14 @@ body.layout-modern .tag-controls {
/* Delete fade-out */ /* Delete fade-out */
@keyframes dm-msg-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 { .dm-msg-deleting {
@@ -11133,6 +11140,64 @@ body.layout-modern .tag-controls {
overflow: hidden; 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 ───────────────────────────────────────────── */ /* ── Send form ───────────────────────────────────────────── */
.dm-send-form { .dm-send-form {
gap: 8px; gap: 8px;

View File

@@ -305,6 +305,7 @@ if (window.__dmLoaded) {
const renderedIds = new Set(); const renderedIds = new Set();
let threadMessages = []; // Cache for re-rendering (e.g. emojis) let threadMessages = []; // Cache for re-rendering (e.g. emojis)
const dmPostPreviewCache = new Map(); // itemId → { item, meta } | null const dmPostPreviewCache = new Map(); // itemId → { item, meta } | null
let activeReply = null; // { senderName, preview } | null
// Title management — global across all pages // Title management — global across all pages
let _dmTitleCount = 0; let _dmTitleCount = 0;
@@ -718,21 +719,24 @@ if (window.__dmLoaded) {
`<div class="dm-bubble comment-content">${content}</div>` + `<div class="dm-bubble comment-content">${content}</div>` +
`<span class="dm-msg-time" data-ts="${escHtml(m.created_at)}">${escHtml(time)}${editedBit}</span>`; `<span class="dm-msg-time" data-ts="${escHtml(m.created_at)}">${escHtml(time)}${editedBit}</span>`;
if (isMine) { // Actions row — always shown (reply for all, edit/delete for own)
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'dm-msg-actions'; actions.className = 'dm-msg-actions';
actions.innerHTML =
`<button class="dm-msg-action-btn" data-action="edit" title="Edit"><i class="fa-solid fa-pen-to-square"></i></button>` + const replyBtn = `<button class="dm-msg-action-btn" data-action="reply" title="Reply"><i class="fa-solid fa-reply"></i></button>`;
`<button class="dm-msg-action-btn" data-action="delete" title="Delete"><i class="fa-solid fa-trash"></i></button>`; const editBtn = `<button class="dm-msg-action-btn" data-action="edit" title="Edit"><i class="fa-solid fa-pen-to-square"></i></button>`;
const delBtn = `<button class="dm-msg-action-btn" data-action="delete" title="Delete"><i class="fa-solid fa-trash"></i></button>`;
actions.innerHTML = replyBtn + (isMine ? editBtn + delBtn : '');
actions.addEventListener('click', async (e) => { actions.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-action]'); const btn = e.target.closest('[data-action]');
if (!btn) return; if (!btn) return;
const action = btn.dataset.action; const action = btn.dataset.action;
if (action === 'reply') replyToDmMessage(m, div);
if (action === 'edit') editDmMessage(m, div); if (action === 'edit') editDmMessage(m, div);
if (action === 'delete') deleteDmMessage(m, div); if (action === 'delete') deleteDmMessage(m, div);
}); });
div.appendChild(actions); div.appendChild(actions);
}
// Async: extract post IDs from raw plaintext and inject preview cards into the bubble // Async: extract post IDs from raw plaintext and inject preview cards into the bubble
if (m.plaintext) resolvePostPreviews(div, m.plaintext); if (m.plaintext) resolvePostPreviews(div, m.plaintext);
@@ -743,6 +747,62 @@ if (window.__dmLoaded) {
return div; 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 =
`<span class="dm-reply-banner__icon"><i class="fa-solid fa-reply"></i></span>` +
`<span class="dm-reply-banner__body"><strong>${name}</strong> ${preview}</span>` +
`<button class="dm-reply-banner__close" title="Cancel reply"><i class="fa-solid fa-xmark"></i></button>`;
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) { async function editDmMessage(m, div) {
if (div.dataset.editing === 'true') return; if (div.dataset.editing === 'true') return;
if (!m.plaintext) { showFlashMsg('Cannot edit — message could not be decrypted', 'error'); 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) => { form.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
if (sendInFlight) return; // prevent double-submit if (sendInFlight) return; // prevent double-submit
const text = input.value.trim(); const rawText = input.value.trim();
if (!text) return; 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 recipient has no key, block sending — we cannot encrypt for them
if (!currentOtherPubKey) { if (!currentOtherPubKey) {
@@ -1692,6 +1757,7 @@ if (window.__dmLoaded) {
if (res.success) { if (res.success) {
input.value = ''; input.value = '';
input.style.height = ''; input.style.height = '';
clearReply();
// Optimistic render with the real server ID so dedup works // Optimistic render with the real server ID so dedup works
const thread = document.getElementById('dm-thread'); const thread = document.getElementById('dm-thread');