Files
ShooterHub/frontend/js/messages.js
2026-04-02 11:24:30 +02:00

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();