add decent replying to direct messages
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>`;
|
||||||
actions.addEventListener('click', async (e) => {
|
const delBtn = `<button class="dm-msg-action-btn" data-action="delete" title="Delete"><i class="fa-solid fa-trash"></i></button>`;
|
||||||
const btn = e.target.closest('[data-action]');
|
|
||||||
if (!btn) return;
|
actions.innerHTML = replyBtn + (isMine ? editBtn + delBtn : '');
|
||||||
const action = btn.dataset.action;
|
actions.addEventListener('click', async (e) => {
|
||||||
if (action === 'edit') editDmMessage(m, div);
|
const btn = e.target.closest('[data-action]');
|
||||||
if (action === 'delete') deleteDmMessage(m, div);
|
if (!btn) return;
|
||||||
});
|
const action = btn.dataset.action;
|
||||||
div.appendChild(actions);
|
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
|
// 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');
|
||||||
|
|||||||
Reference in New Issue
Block a user