273 lines
11 KiB
JavaScript
273 lines
11 KiB
JavaScript
// ── Messages page ─────────────────────────────────────────────────────────────
|
|
|
|
const composeModal = new bootstrap.Modal('#composeModal');
|
|
|
|
let _currentTab = 'inbox'; // 'inbox' | 'sent'
|
|
let _messages = []; // current list
|
|
let _openId = null; // currently open message id
|
|
let _recipientId = null; // chosen recipient in compose
|
|
|
|
// ── Tab switching ─────────────────────────────────────────────────────────────
|
|
|
|
document.querySelectorAll('#msgTabs .nav-link').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('#msgTabs .nav-link').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
_currentTab = btn.dataset.tab;
|
|
_openId = null;
|
|
showDetail(null);
|
|
loadMessages();
|
|
});
|
|
});
|
|
|
|
// ── Load messages ─────────────────────────────────────────────────────────────
|
|
|
|
async function loadMessages() {
|
|
document.getElementById('msgList').innerHTML =
|
|
'<div class="text-center py-4 text-muted small">Loading…</div>';
|
|
|
|
try {
|
|
const url = _currentTab === 'sent' ? '/social/messages/sent/' : '/social/messages/';
|
|
_messages = await apiGet(url);
|
|
renderList();
|
|
loadUnreadCount();
|
|
} catch (e) {
|
|
document.getElementById('msgList').innerHTML =
|
|
'<div class="text-center py-3 text-danger small">Failed to load messages.</div>';
|
|
}
|
|
}
|
|
|
|
function renderList() {
|
|
const el = document.getElementById('msgList');
|
|
if (!_messages.length) {
|
|
el.innerHTML = '<div class="text-center py-4 text-muted small">No messages here yet.</div>';
|
|
return;
|
|
}
|
|
el.innerHTML = _messages.map(m => {
|
|
const other = _currentTab === 'sent' ? m.recipient_detail : m.sender_detail;
|
|
const isUnread = _currentTab === 'inbox' && !m.read_at;
|
|
const date = m.sent_at ? new Date(m.sent_at).toLocaleDateString() : '';
|
|
return `
|
|
<div class="msg-row d-flex align-items-start gap-2 ${isUnread ? 'unread' : ''} ${m.id === _openId ? 'active' : ''}"
|
|
data-id="${m.id}" onclick="openMessage(${m.id})">
|
|
${isUnread ? '<span class="unread-dot mt-1 flex-shrink-0"></span>' : '<span style="width:8px;flex-shrink:0"></span>'}
|
|
<div class="overflow-hidden flex-grow-1">
|
|
<div class="d-flex justify-content-between">
|
|
<span class="msg-subject">${esc(m.subject)}</span>
|
|
<span class="msg-meta ms-2">${esc(date)}</span>
|
|
</div>
|
|
<div class="msg-meta">${esc(other?.display_name || other?.username || '—')}</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// ── Open a message ────────────────────────────────────────────────────────────
|
|
|
|
async function openMessage(id) {
|
|
_openId = id;
|
|
// Highlight
|
|
document.querySelectorAll('.msg-row').forEach(r => {
|
|
r.classList.toggle('active', Number(r.dataset.id) === id);
|
|
});
|
|
|
|
try {
|
|
const msg = await apiGet(`/social/messages/${id}/`);
|
|
showDetail(msg);
|
|
|
|
// Mark as read visually
|
|
const row = document.querySelector(`.msg-row[data-id="${id}"]`);
|
|
if (row) row.classList.remove('unread');
|
|
// Refresh unread badge
|
|
loadUnreadCount();
|
|
} catch (e) {
|
|
showToast('Failed to load message.', 'danger');
|
|
}
|
|
}
|
|
|
|
function showDetail(msg) {
|
|
const empty = document.getElementById('detailEmpty');
|
|
const content = document.getElementById('detailContent');
|
|
if (!msg) {
|
|
empty.classList.remove('d-none');
|
|
content.classList.add('d-none');
|
|
return;
|
|
}
|
|
empty.classList.add('d-none');
|
|
content.classList.remove('d-none');
|
|
|
|
document.getElementById('detailSubject').textContent = msg.subject;
|
|
document.getElementById('detailBody').textContent = msg.body;
|
|
|
|
const from = msg.sender_detail?.display_name || msg.sender_detail?.username || '—';
|
|
const to = msg.recipient_detail?.display_name || msg.recipient_detail?.username || '—';
|
|
const date = msg.sent_at ? new Date(msg.sent_at).toLocaleString() : '';
|
|
document.getElementById('detailMeta').innerHTML = `
|
|
<span><i class="bi bi-person me-1"></i>From: <strong>${esc(from)}</strong></span>
|
|
<span><i class="bi bi-person-fill-check me-1"></i>To: <strong>${esc(to)}</strong></span>
|
|
<span><i class="bi bi-clock me-1"></i>${esc(date)}</span>`;
|
|
|
|
// Reply button prefills compose
|
|
document.getElementById('detailReplyBtn').onclick = () => {
|
|
if (_currentTab === 'inbox' && msg.sender_detail) {
|
|
prefillCompose(msg.sender_detail, `Re: ${msg.subject}`);
|
|
} else {
|
|
prefillCompose(msg.recipient_detail, `Re: ${msg.subject}`);
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── Delete ────────────────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('detailDeleteBtn').addEventListener('click', async () => {
|
|
if (!_openId || !confirm('Delete this message?')) return;
|
|
try {
|
|
await apiDelete(`/social/messages/${_openId}/`);
|
|
_messages = _messages.filter(m => m.id !== _openId);
|
|
_openId = null;
|
|
renderList();
|
|
showDetail(null);
|
|
showToast('Message deleted.');
|
|
} catch (e) {
|
|
showToast('Delete failed.', 'danger');
|
|
}
|
|
});
|
|
|
|
// ── Compose ───────────────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('composeBtn').addEventListener('click', () => openCompose());
|
|
|
|
function openCompose(recipientDetail = null, subject = '') {
|
|
clearCompose();
|
|
if (recipientDetail) prefillCompose(recipientDetail, subject);
|
|
composeModal.show();
|
|
}
|
|
|
|
function prefillCompose(recipientDetail, subject) {
|
|
clearCompose();
|
|
_recipientId = recipientDetail.id;
|
|
document.getElementById('recipientId').value = recipientDetail.id;
|
|
document.getElementById('recipientLabel').textContent = recipientDetail.display_name || recipientDetail.username;
|
|
document.getElementById('recipientChosen').classList.remove('d-none');
|
|
document.getElementById('recipientSearch').disabled = true;
|
|
document.getElementById('composeSubject').value = subject;
|
|
composeModal.show();
|
|
}
|
|
|
|
function clearCompose() {
|
|
_recipientId = null;
|
|
document.getElementById('recipientId').value = '';
|
|
document.getElementById('recipientSearch').value = '';
|
|
document.getElementById('recipientSearch').disabled = false;
|
|
document.getElementById('recipientResults').classList.add('d-none');
|
|
document.getElementById('recipientResults').innerHTML = '';
|
|
document.getElementById('recipientChosen').classList.add('d-none');
|
|
document.getElementById('recipientLabel').textContent = '';
|
|
document.getElementById('composeSubject').value = '';
|
|
document.getElementById('composeBody').value = '';
|
|
document.getElementById('composeAlert').classList.add('d-none');
|
|
}
|
|
|
|
document.getElementById('recipientClear').addEventListener('click', () => {
|
|
_recipientId = null;
|
|
document.getElementById('recipientId').value = '';
|
|
document.getElementById('recipientChosen').classList.add('d-none');
|
|
document.getElementById('recipientSearch').disabled = false;
|
|
document.getElementById('recipientSearch').value = '';
|
|
document.getElementById('recipientSearch').focus();
|
|
});
|
|
|
|
// Recipient search autocomplete
|
|
let _searchTimer = null;
|
|
document.getElementById('recipientSearch').addEventListener('input', function () {
|
|
clearTimeout(_searchTimer);
|
|
const q = this.value.trim();
|
|
if (q.length < 2) {
|
|
document.getElementById('recipientResults').classList.add('d-none');
|
|
return;
|
|
}
|
|
_searchTimer = setTimeout(async () => {
|
|
try {
|
|
const results = await apiGet(`/social/members/?q=${encodeURIComponent(q)}`);
|
|
renderRecipientResults(results);
|
|
} catch (e) { /* ignore */ }
|
|
}, 300);
|
|
});
|
|
|
|
function renderRecipientResults(results) {
|
|
const el = document.getElementById('recipientResults');
|
|
if (!results.length) { el.classList.add('d-none'); return; }
|
|
el.innerHTML = results.map(u =>
|
|
`<button type="button" class="list-group-item list-group-item-action py-1 px-2 small"
|
|
data-uid="${u.id}" data-name="${esc(u.display_name || u.username)}">
|
|
<strong>${esc(u.display_name || u.username)}</strong>
|
|
<span class="text-muted ms-1">@${esc(u.username)}</span>
|
|
</button>`
|
|
).join('');
|
|
el.classList.remove('d-none');
|
|
|
|
el.querySelectorAll('button').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
_recipientId = Number(btn.dataset.uid);
|
|
document.getElementById('recipientId').value = _recipientId;
|
|
document.getElementById('recipientLabel').textContent = btn.dataset.name;
|
|
document.getElementById('recipientChosen').classList.remove('d-none');
|
|
document.getElementById('recipientSearch').disabled = true;
|
|
document.getElementById('recipientSearch').value = '';
|
|
el.classList.add('d-none');
|
|
});
|
|
});
|
|
}
|
|
|
|
document.getElementById('composeSendBtn').addEventListener('click', async () => {
|
|
const alertEl = document.getElementById('composeAlert');
|
|
alertEl.classList.add('d-none');
|
|
|
|
if (!_recipientId) {
|
|
alertEl.textContent = 'Please select a recipient.';
|
|
alertEl.classList.remove('d-none');
|
|
return;
|
|
}
|
|
const subject = document.getElementById('composeSubject').value.trim();
|
|
const body = document.getElementById('composeBody').value.trim();
|
|
if (!subject) { alertEl.textContent = 'Subject is required.'; alertEl.classList.remove('d-none'); return; }
|
|
if (!body) { alertEl.textContent = 'Message body is required.'; alertEl.classList.remove('d-none'); return; }
|
|
|
|
const btn = document.getElementById('composeSendBtn');
|
|
const spin = document.getElementById('composeSpin');
|
|
btn.disabled = true;
|
|
spin.classList.remove('d-none');
|
|
|
|
try {
|
|
await apiPost('/social/messages/', { recipient: _recipientId, subject, body });
|
|
composeModal.hide();
|
|
showToast('Message sent.');
|
|
if (_currentTab === 'sent') loadMessages();
|
|
} catch (e) {
|
|
alertEl.textContent = 'Failed to send message.';
|
|
alertEl.classList.remove('d-none');
|
|
} finally {
|
|
btn.disabled = false;
|
|
spin.classList.add('d-none');
|
|
}
|
|
});
|
|
|
|
// ── Unread count ──────────────────────────────────────────────────────────────
|
|
|
|
async function loadUnreadCount() {
|
|
try {
|
|
const data = await apiGet('/social/messages/unread-count/');
|
|
const badge = document.getElementById('unreadBadge');
|
|
if (data.unread > 0) {
|
|
badge.textContent = data.unread;
|
|
badge.classList.remove('d-none');
|
|
} else {
|
|
badge.classList.add('d-none');
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
// ── Boot ──────────────────────────────────────────────────────────────────────
|
|
|
|
loadMessages();
|