add option to edit/delete direct mssages and dynamic expiry date for attachments via config
This commit is contained in:
@@ -62,6 +62,7 @@
|
|||||||
"enable_danmaku": true,
|
"enable_danmaku": true,
|
||||||
"private_messages": true,
|
"private_messages": true,
|
||||||
"dm_attachments": true,
|
"dm_attachments": true,
|
||||||
|
"dm_attachment_expiry_days": 90,
|
||||||
"halls_enabled": true,
|
"halls_enabled": true,
|
||||||
"userhalls_enabled": true,
|
"userhalls_enabled": true,
|
||||||
"enable_userhall_image_upload": true,
|
"enable_userhall_image_upload": true,
|
||||||
|
|||||||
@@ -2770,11 +2770,15 @@ CREATE TABLE IF NOT EXISTS public.dm_attachments (
|
|||||||
mime_hint text NOT NULL DEFAULT '',
|
mime_hint text NOT NULL DEFAULT '',
|
||||||
size_bytes integer NOT NULL DEFAULT 0,
|
size_bytes integer NOT NULL DEFAULT 0,
|
||||||
created_at timestamp with time zone DEFAULT now(),
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
expires_at timestamp with time zone DEFAULT (now() + interval '30 days')
|
expires_at timestamp with time zone DEFAULT (now() + interval '90 days')
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_dm_att_sender ON public.dm_attachments(sender_id);
|
CREATE INDEX IF NOT EXISTS idx_dm_att_sender ON public.dm_attachments(sender_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_dm_att_recipient ON public.dm_attachments(recipient_id);
|
CREATE INDEX IF NOT EXISTS idx_dm_att_recipient ON public.dm_attachments(recipient_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_dm_att_expires ON public.dm_attachments(expires_at);
|
CREATE INDEX IF NOT EXISTS idx_dm_att_expires ON public.dm_attachments(expires_at);
|
||||||
|
|
||||||
|
-- DM message edit/delete support (Migration 006)
|
||||||
|
ALTER TABLE private_messages
|
||||||
|
ADD COLUMN IF NOT EXISTS edited_at timestamp with time zone DEFAULT NULL;
|
||||||
|
|
||||||
\unrestrict RMNKNzVQLV2ZcwmM3bmhglTot5nRoju9FmRyi3eUMfNy6iJUBfHRIgXnbrpJikG
|
\unrestrict RMNKNzVQLV2ZcwmM3bmhglTot5nRoju9FmRyi3eUMfNy6iJUBfHRIgXnbrpJikG
|
||||||
|
|||||||
@@ -11030,6 +11030,109 @@ body.layout-modern .tag-controls {
|
|||||||
font-size: 0.88em;
|
font-size: 0.88em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Message edit / delete actions ─────────────────────── */
|
||||||
|
.dm-msg {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-msg-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-msg:hover .dm-msg-actions,
|
||||||
|
.dm-msg:focus-within .dm-msg-actions {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-msg-action-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #555;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.78em;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.12s, background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-msg-action-btn:hover {
|
||||||
|
color: var(--fg, #ddd);
|
||||||
|
background: rgba(255,255,255,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-msg-action-btn[data-action="delete"]:hover {
|
||||||
|
color: #e55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline edit textarea */
|
||||||
|
.dm-edit-wrap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-edit-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
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;
|
||||||
|
font-size: 0.92em;
|
||||||
|
padding: 6px 8px;
|
||||||
|
resize: vertical;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-edit-save,
|
||||||
|
.dm-edit-cancel {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-edit-save {
|
||||||
|
background: var(--badge-bg, #4a90d9);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-edit-cancel {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
color: var(--fg, #ddd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (edited) badge */
|
||||||
|
.dm-edited {
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.85em;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete fade-out */
|
||||||
|
@keyframes dm-msg-fade-out {
|
||||||
|
to { opacity: 0; transform: scaleY(0); max-height: 0; margin: 0; padding: 0; overflow: hidden; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-msg-deleting {
|
||||||
|
animation: dm-msg-fade-out 0.25s ease forwards;
|
||||||
|
transform-origin: top;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Send form ───────────────────────────────────────────── */
|
/* ── Send form ───────────────────────────────────────────── */
|
||||||
.dm-send-form {
|
.dm-send-form {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -710,9 +710,29 @@ if (window.__dmLoaded) {
|
|||||||
const hasEmbed = content.includes('yt-embed-wrap');
|
const hasEmbed = content.includes('yt-embed-wrap');
|
||||||
div.className = `dm-msg ${isMine ? 'dm-msg-mine' : 'dm-msg-theirs'}${hasEmbed ? ' dm-has-embed' : ''}`;
|
div.className = `dm-msg ${isMine ? 'dm-msg-mine' : 'dm-msg-theirs'}${hasEmbed ? ' dm-has-embed' : ''}`;
|
||||||
div.dataset.msgId = m.id;
|
div.dataset.msgId = m.id;
|
||||||
|
if (m.plaintext) div.dataset.plaintext = m.plaintext;
|
||||||
|
|
||||||
const time = timeAgo(m.created_at);
|
const time = timeAgo(m.created_at);
|
||||||
div.innerHTML = `<div class="dm-bubble comment-content">${content}</div><span class="dm-msg-time" data-ts="${escHtml(m.created_at)}">${escHtml(time)}</span>`;
|
const editedBit = m.edited_at ? ' <span class="dm-edited">(edited)</span>' : '';
|
||||||
|
div.innerHTML =
|
||||||
|
`<div class="dm-bubble comment-content">${content}</div>` +
|
||||||
|
`<span class="dm-msg-time" data-ts="${escHtml(m.created_at)}">${escHtml(time)}${editedBit}</span>`;
|
||||||
|
|
||||||
|
if (isMine) {
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
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>` +
|
||||||
|
`<button class="dm-msg-action-btn" data-action="delete" title="Delete"><i class="fa-solid fa-trash"></i></button>`;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -723,6 +743,127 @@ if (window.__dmLoaded) {
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function editDmMessage(m, div) {
|
||||||
|
if (div.dataset.editing === 'true') return;
|
||||||
|
if (!m.plaintext) { showFlashMsg('Cannot edit — message could not be decrypted', 'error'); return; }
|
||||||
|
div.dataset.editing = 'true';
|
||||||
|
|
||||||
|
const bubble = div.querySelector('.dm-bubble');
|
||||||
|
const origHtml = bubble.innerHTML;
|
||||||
|
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'dm-edit-wrap';
|
||||||
|
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.className = 'dm-edit-textarea';
|
||||||
|
ta.value = m.plaintext;
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'dm-edit-actions';
|
||||||
|
|
||||||
|
const saveBtn = document.createElement('button');
|
||||||
|
saveBtn.className = 'dm-edit-save';
|
||||||
|
saveBtn.textContent = 'Save';
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.className = 'dm-edit-cancel';
|
||||||
|
cancelBtn.textContent = 'Cancel';
|
||||||
|
|
||||||
|
actions.append(saveBtn, cancelBtn);
|
||||||
|
wrap.append(ta, actions);
|
||||||
|
bubble.innerHTML = '';
|
||||||
|
bubble.appendChild(wrap);
|
||||||
|
ta.focus();
|
||||||
|
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
bubble.innerHTML = origHtml;
|
||||||
|
div.dataset.editing = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', async () => {
|
||||||
|
const newText = ta.value.trim();
|
||||||
|
if (!newText || newText === m.plaintext) {
|
||||||
|
bubble.innerHTML = origHtml;
|
||||||
|
div.dataset.editing = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!currentOtherPubKey) { showFlashMsg('Cannot encrypt: no key', 'error'); return; }
|
||||||
|
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.textContent = '…';
|
||||||
|
try {
|
||||||
|
const sharedKey = await deriveSharedKey(_privateKey, currentOtherPubKey);
|
||||||
|
const { iv: newIv, ciphertext: newCt } = await encryptMessage(sharedKey, newText);
|
||||||
|
|
||||||
|
const res = await fetch(`/api/dm/message/${m.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrfToken() },
|
||||||
|
body: new URLSearchParams({ ciphertext: newCt, iv: newIv })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.success) { showFlashMsg('Edit failed: ' + (data.msg || res.status), 'error'); return; }
|
||||||
|
|
||||||
|
// Update in-memory plaintext and re-render bubble
|
||||||
|
m.plaintext = newText;
|
||||||
|
m.edited_at = data.edited_at || new Date().toISOString();
|
||||||
|
div.dataset.plaintext = newText;
|
||||||
|
|
||||||
|
const newContent = renderDmContent(newText).trimEnd();
|
||||||
|
const editedBit = '<span class="dm-edited">(edited)</span>';
|
||||||
|
bubble.innerHTML = newContent;
|
||||||
|
|
||||||
|
// Update the time span
|
||||||
|
const timeEl = div.querySelector('.dm-msg-time');
|
||||||
|
if (timeEl) timeEl.innerHTML = escHtml(timeAgo(m.created_at)) + ' ' + editedBit;
|
||||||
|
|
||||||
|
if (m.plaintext) resolvePostPreviews(div, m.plaintext);
|
||||||
|
if (m.plaintext && m.plaintext.includes('[attachment:')) resolveAttachments(div, m.plaintext);
|
||||||
|
} catch (e) {
|
||||||
|
showFlashMsg('Edit error: ' + e.message, 'error');
|
||||||
|
} finally {
|
||||||
|
div.dataset.editing = '';
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDmMessage(m, div) {
|
||||||
|
if (div.dataset.deleting === 'true') return;
|
||||||
|
div.dataset.deleting = 'true';
|
||||||
|
|
||||||
|
// Extract attachment IDs from plaintext sentinels
|
||||||
|
const attachmentIds = [];
|
||||||
|
if (m.plaintext) {
|
||||||
|
for (const att of parseAttachmentSentinels(m.plaintext)) {
|
||||||
|
const id = parseInt(att.id, 10);
|
||||||
|
if (id) attachmentIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = new URLSearchParams();
|
||||||
|
attachmentIds.forEach(id => body.append('attachment_ids[]', id));
|
||||||
|
|
||||||
|
const res = await fetch(`/api/dm/message/${m.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrfToken() },
|
||||||
|
body
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.success) {
|
||||||
|
showFlashMsg('Delete failed: ' + (data.msg || res.status), 'error');
|
||||||
|
div.dataset.deleting = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.classList.add('dm-msg-deleting');
|
||||||
|
div.addEventListener('animationend', () => div.remove(), { once: true });
|
||||||
|
} catch (e) {
|
||||||
|
showFlashMsg('Delete error: ' + e.message, 'error');
|
||||||
|
div.dataset.deleting = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Post link preview cards ───────────────────────────────────────────────
|
// ── Post link preview cards ───────────────────────────────────────────────
|
||||||
// Extracts item IDs from the raw plaintext (immune to rendering pipeline
|
// Extracts item IDs from the raw plaintext (immune to rendering pipeline
|
||||||
// variations: marked / commentSystem / plain-text fallback), then appends
|
// variations: marked / commentSystem / plain-text fallback), then appends
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import db from './inc/sql.mjs';
|
|||||||
import lib from './inc/lib.mjs';
|
import lib from './inc/lib.mjs';
|
||||||
import cfg from './inc/config.mjs';
|
import cfg from './inc/config.mjs';
|
||||||
import { collectBody } from './inc/multipart.mjs';
|
import { collectBody } from './inc/multipart.mjs';
|
||||||
import { getDmAttachments } from './inc/settings.mjs';
|
import { getDmAttachments, getDmAttachmentExpiryDays } from './inc/settings.mjs';
|
||||||
|
|
||||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -165,6 +165,7 @@ export async function handleDmAttachmentUpload(req, res, recipientId) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Insert DB record first to get an ID
|
// Insert DB record first to get an ID
|
||||||
|
const expiresAt = new Date(Date.now() + getDmAttachmentExpiryDays() * 86400000);
|
||||||
const [row] = await db`
|
const [row] = await db`
|
||||||
INSERT INTO dm_attachments ${db({
|
INSERT INTO dm_attachments ${db({
|
||||||
sender_id: session.id,
|
sender_id: session.id,
|
||||||
@@ -173,7 +174,8 @@ export async function handleDmAttachmentUpload(req, res, recipientId) {
|
|||||||
file_path: '',
|
file_path: '',
|
||||||
original_name: originalName,
|
original_name: originalName,
|
||||||
mime_hint: mimeHint,
|
mime_hint: mimeHint,
|
||||||
size_bytes: sizeBytes
|
size_bytes: sizeBytes,
|
||||||
|
expires_at: expiresAt
|
||||||
})}
|
})}
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -209,7 +209,8 @@ export default (router, tpl) => {
|
|||||||
pm.ciphertext,
|
pm.ciphertext,
|
||||||
pm.iv,
|
pm.iv,
|
||||||
pm.is_read,
|
pm.is_read,
|
||||||
pm.created_at
|
pm.created_at,
|
||||||
|
pm.edited_at
|
||||||
FROM private_messages pm
|
FROM private_messages pm
|
||||||
WHERE (
|
WHERE (
|
||||||
(pm.sender_id = ${req.session.id} AND pm.recipient_id = ${otherId})
|
(pm.sender_id = ${req.session.id} AND pm.recipient_id = ${otherId})
|
||||||
@@ -312,6 +313,81 @@ export default (router, tpl) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edit own message (re-encrypt in browser, send new ciphertext + iv)
|
||||||
|
router.patch(/\/api\/dm\/message\/(?<msgId>\d+)/, async (req, res) => {
|
||||||
|
if (!getPrivateMessages()) return json(res, { success: false, msg: 'Not found' }, 404);
|
||||||
|
if (!req.session) return json(res, { success: false, msg: 'Unauthorized' }, 401);
|
||||||
|
|
||||||
|
const csrf = req.headers['x-csrf-token'];
|
||||||
|
if (!csrf || csrf !== req.session.csrf_token) return json(res, { success: false, msg: 'CSRF mismatch' }, 403);
|
||||||
|
|
||||||
|
const msgId = parseInt(req.params.msgId, 10);
|
||||||
|
const body = req.post || {};
|
||||||
|
const { ciphertext, iv } = body;
|
||||||
|
|
||||||
|
if (!ciphertext || !iv || typeof ciphertext !== 'string' || typeof iv !== 'string') {
|
||||||
|
return json(res, { success: false, msg: 'Missing ciphertext or iv' }, 400);
|
||||||
|
}
|
||||||
|
if (ciphertext.length > 65536 || iv.length > 32) {
|
||||||
|
return json(res, { success: false, msg: 'Payload too large' }, 413);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db`
|
||||||
|
UPDATE private_messages
|
||||||
|
SET ciphertext = ${ciphertext}, iv = ${iv}, edited_at = NOW()
|
||||||
|
WHERE id = ${msgId} AND sender_id = ${req.session.id}
|
||||||
|
RETURNING id, edited_at
|
||||||
|
`;
|
||||||
|
if (!result.length) return json(res, { success: false, msg: 'Not found or not yours' }, 404);
|
||||||
|
return json(res, { success: true, id: result[0].id, edited_at: result[0].edited_at });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DM] edit message failed:', err);
|
||||||
|
return json(res, { success: false, msg: 'DB error' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete own message (and optionally its attachment blobs)
|
||||||
|
router.delete(/\/api\/dm\/message\/(?<msgId>\d+)/, async (req, res) => {
|
||||||
|
if (!getPrivateMessages()) return json(res, { success: false, msg: 'Not found' }, 404);
|
||||||
|
if (!req.session) return json(res, { success: false, msg: 'Unauthorized' }, 401);
|
||||||
|
|
||||||
|
const csrf = req.headers['x-csrf-token'];
|
||||||
|
if (!csrf || csrf !== req.session.csrf_token) return json(res, { success: false, msg: 'CSRF mismatch' }, 403);
|
||||||
|
|
||||||
|
const msgId = parseInt(req.params.msgId, 10);
|
||||||
|
const body = req.post || {};
|
||||||
|
const rawIds = body['attachment_ids[]'] ?? body.attachment_ids;
|
||||||
|
const attachmentIds = (Array.isArray(rawIds) ? rawIds : rawIds ? [rawIds] : [])
|
||||||
|
.map(Number).filter(n => Number.isFinite(n) && n > 0);
|
||||||
|
|
||||||
|
// Verify message belongs to sender
|
||||||
|
const rows = await db`SELECT id FROM private_messages WHERE id = ${msgId} AND sender_id = ${req.session.id} LIMIT 1`;
|
||||||
|
if (!rows.length) return json(res, { success: false, msg: 'Not found or not yours' }, 404);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clean up attachments the client identified (verify sender ownership server-side)
|
||||||
|
if (attachmentIds.length) {
|
||||||
|
const { promises: fsP } = await import('fs');
|
||||||
|
const atts = await db`
|
||||||
|
SELECT id, file_path FROM dm_attachments
|
||||||
|
WHERE id = ANY(${attachmentIds}) AND sender_id = ${req.session.id}
|
||||||
|
`;
|
||||||
|
for (const att of atts) await fsP.unlink(att.file_path).catch(() => {});
|
||||||
|
if (atts.length) {
|
||||||
|
const ids = atts.map(a => a.id);
|
||||||
|
await db`DELETE FROM dm_attachments WHERE id = ANY(${ids})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db`DELETE FROM private_messages WHERE id = ${msgId} AND sender_id = ${req.session.id}`;
|
||||||
|
return json(res, { success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DM] delete message failed:', err);
|
||||||
|
return json(res, { success: false, msg: 'DB error' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Hide a whole conversation (Close DM)
|
// Hide a whole conversation (Close DM)
|
||||||
router.post(/\/api\/dm\/conversation\/(?<userId>\d+)\/delete/, async (req, res) => {
|
router.post(/\/api\/dm\/conversation\/(?<userId>\d+)\/delete/, async (req, res) => {
|
||||||
if (!getPrivateMessages()) return json(res, { success: false, msg: 'Not found' }, 404);
|
if (!getPrivateMessages()) return json(res, { success: false, msg: 'Not found' }, 404);
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ export const setPrivateMessages = (val) => private_messages = !!val;
|
|||||||
export const getDmAttachments = () => dm_attachments;
|
export const getDmAttachments = () => dm_attachments;
|
||||||
export const setDmAttachments = (val) => dm_attachments = !!val;
|
export const setDmAttachments = (val) => dm_attachments = !!val;
|
||||||
|
|
||||||
|
export const getDmAttachmentExpiryDays = () => {
|
||||||
|
const v = parseInt(cfg.websrv.dm_attachment_expiry_days);
|
||||||
|
return (Number.isFinite(v) && v > 0) ? v : 90;
|
||||||
|
};
|
||||||
|
|
||||||
export const getDefaultLayout = () => default_layout;
|
export const getDefaultLayout = () => default_layout;
|
||||||
export const setDefaultLayout = (val) => default_layout = (val === 'legacy' ? 'legacy' : 'modern');
|
export const setDefaultLayout = (val) => default_layout = (val === 'legacy' ? 'legacy' : 'modern');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user