/* ============================================================
BAN YARO — Chat-Seite
============================================================ */
window.Page_chat = (() => {
let _container = null;
let _view = 'list'; // 'list' | 'thread'
let _convId = null;
let _partnerName = '';
let _myId = null;
let _pollTimer = null;
let _heartbeatTimer = null;
let _lastMsgId = 0;
// ----------------------------------------------------------
async function init(container, appState, params = {}) {
_container = container;
_myId = appState?.user?.id || null;
// Heartbeat: alle 30s online-Status senden
API.chat.heartbeat().catch(() => {});
_heartbeatTimer = setInterval(() => {
API.chat.heartbeat().catch(() => {});
}, 30000);
if (params.conversation_id) {
await _openThread(params.conversation_id);
} else {
await _showList();
}
}
// ----------------------------------------------------------
// Conversation list
// ----------------------------------------------------------
async function _showList() {
_view = 'list';
_stopPolling();
_convId = null;
_container.innerHTML = `
Nachrichten
`;
document.getElementById('chat-new-btn')?.addEventListener('click', _showNewMessagePicker);
await _loadList();
await _updateChatBadge();
}
async function _loadList() {
const el = document.getElementById('chat-list-body');
if (!el) return;
try {
const convs = await API.chat.conversations();
if (!convs.length) {
el.innerHTML = `
Noch keine Nachrichten.
Schreibe einem Freund über die Freunde-Seite!
`;
return;
}
el.innerHTML = convs.map(c => {
const initials = (c.partner_name || '?')[0].toUpperCase();
const preview = c.last_text
? _esc(c.last_text.substring(0, 60)) + (c.last_text.length > 60 ? '…' : '')
: 'Noch keine Nachrichten';
const timeStr = c.last_msg_at ? _fmtTime(c.last_msg_at) : '';
const badge = c.unread_count > 0
? `${c.unread_count}`
: '';
const onlineDot = c.partner_online
? ``
: '';
return `
${initials}
${onlineDot ? `
` : ''}
${_esc(c.partner_name)}
${preview}
${timeStr}
${badge}
`;
}).join('');
} catch (e) {
if (e.status === 401) {
document.getElementById('chat-list-body').innerHTML =
``;
}
}
}
// ----------------------------------------------------------
// Thread (message view)
// ----------------------------------------------------------
async function _openThread(convId) {
_convId = convId;
_view = 'thread';
_stopPolling();
_container.innerHTML = `
`;
// Auto-resize textarea
const input = document.getElementById('chat-input');
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 100) + 'px';
});
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
Page_chat._send();
}
});
await _loadMessages(true);
await API.chat.markRead(_convId).catch(() => {});
await _updateChatBadge();
// Poll every 4s while thread is open
_pollTimer = setInterval(async () => {
if (_view === 'thread' && _convId === convId) {
await _pollNew();
}
}, 4000);
}
async function _loadMessages(scroll = false) {
const el = document.getElementById('chat-messages');
if (!el) return;
try {
const data = await API.chat.messages(_convId);
_partnerName = data.partner_name;
const nameEl = document.getElementById('chat-partner-name');
const avEl = document.getElementById('chat-partner-av');
const dotEl = document.getElementById('chat-partner-dot');
if (nameEl) nameEl.textContent = data.partner_name;
if (avEl) avEl.textContent = (data.partner_name || '?')[0].toUpperCase();
if (dotEl) dotEl.style.display = data.partner_online ? '' : 'none';
if (!data.messages.length) {
el.innerHTML = `
Schreibe die erste Nachricht!
`;
_lastMsgId = 0;
return;
}
_lastMsgId = data.messages[data.messages.length - 1].id;
el.innerHTML = _renderMessages(data.messages);
if (scroll) _scrollToBottom(el);
} catch (e) {
if (el) el.innerHTML = ``;
}
}
async function _pollNew() {
const el = document.getElementById('chat-messages');
if (!el) return;
try {
const data = await API.chat.messages(_convId);
if (!data.messages.length) return;
const newest = data.messages[data.messages.length - 1];
if (newest.id <= _lastMsgId) return;
// New messages arrived
_lastMsgId = newest.id;
const wasAtBottom = _isScrolledToBottom(el);
el.innerHTML = _renderMessages(data.messages);
if (wasAtBottom) _scrollToBottom(el);
// Online-Dot aktualisieren
const dotEl = document.getElementById('chat-partner-dot');
if (dotEl) dotEl.style.display = data.partner_online ? '' : 'none';
await API.chat.markRead(_convId).catch(() => {});
await _updateChatBadge();
} catch (e) {
// silent
}
}
function _renderMessages(msgs) {
let html = '';
let lastDate = null;
for (const m of msgs) {
const dateStr = m.created_at.substring(0, 10);
if (dateStr !== lastDate) {
html += `${_fmtDate(m.created_at)}
`;
lastDate = dateStr;
}
const isMine = m.sender_id === _myId;
const rowClass = isMine ? 'chat-bubble-row--mine' : 'chat-bubble-row--theirs';
const bubClass = isMine ? 'chat-bubble--mine' : 'chat-bubble--theirs';
const delClass = m.is_deleted ? ' chat-bubble--deleted' : '';
const timeStr = _fmtTime(m.created_at);
const deleteBtn = isMine && !m.is_deleted
? ``
: '';
// Read receipt icon (nur für eigene Nachrichten)
const readIcon = isMine
? (m.read_at
? ``
: ``)
: '';
// Medieninhalt
let bubbleContent = '';
if (m.media_url) {
bubbleContent += `
`;
}
if (m.text) {
bubbleContent += (m.media_url ? `` : '') +
_esc(m.text) +
(m.media_url ? `
` : '');
}
if (!bubbleContent) bubbleContent = _esc(m.text);
html += `
${bubbleContent}
${timeStr} ${readIcon} ${deleteBtn}
`;
}
return html;
}
// ----------------------------------------------------------
async function _send() {
const input = document.getElementById('chat-input');
const btn = document.getElementById('chat-send-btn');
if (!input) return;
const text = input.value.trim();
if (!text || !_convId) return;
btn.disabled = true;
try {
await API.chat.send(_convId, text);
input.value = '';
input.style.height = 'auto';
await _loadMessages(true);
} catch (e) {
UI.toast(e.message, 'danger');
} finally {
btn.disabled = false;
input.focus();
}
}
// ----------------------------------------------------------
async function _deleteMsg(msgId) {
try {
await API.chat.deleteMessage(msgId);
await _loadMessages(false);
} catch (e) {
UI.toast(e.message, 'danger');
}
}
// ----------------------------------------------------------
async function _onPhotoSelected(input) {
const file = input.files && input.files[0];
if (!file || !_convId) return;
input.value = '';
const btn = document.getElementById('chat-send-btn');
if (btn) btn.disabled = true;
try {
await API.chat.uploadPhoto(_convId, file);
await _loadMessages(true);
} catch (e) {
UI.toast(e.message || 'Foto-Upload fehlgeschlagen', 'danger');
} finally {
if (btn) btn.disabled = false;
}
}
// ----------------------------------------------------------
async function _updateChatBadge() {
try {
const convs = await API.chat.conversations();
const total = convs.reduce((s, c) => s + (c.unread_count || 0), 0);
const badge = document.getElementById('chat-badge');
if (badge) {
badge.textContent = total;
badge.style.display = total > 0 ? '' : 'none';
}
} catch (e) {
// silent
}
}
// ----------------------------------------------------------
// Helpers
// ----------------------------------------------------------
function _stopPolling() {
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
}
function _scrollToBottom(el) {
el.scrollTop = el.scrollHeight;
}
function _isScrolledToBottom(el) {
return el.scrollHeight - el.scrollTop - el.clientHeight < 80;
}
function _fmtTime(iso) {
if (!iso) return '';
const d = new Date(iso.replace(' ', 'T') + 'Z');
const now = new Date();
const isToday = d.toDateString() === now.toDateString();
if (isToday) return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
}
function _fmtDate(iso) {
if (!iso) return '';
const d = new Date(iso.replace(' ', 'T') + 'Z');
const now = new Date();
const diff = Math.floor((now - d) / 86400000);
if (diff === 0) return 'Heute';
if (diff === 1) return 'Gestern';
return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' });
}
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"')
.replace(/\n/g, '
');
}
// ----------------------------------------------------------
// Neue Nachricht — Freundesliste als Picker
// ----------------------------------------------------------
async function _showNewMessagePicker() {
let friends = [];
try { friends = (await API.friends.list()).friends || []; } catch {}
if (!friends.length) {
UI.toast.info('Du hast noch keine Freunde. Gehe zu Freunde um jemanden hinzuzufügen.');
return;
}
const items = friends.map(f => `
`).join('');
UI.modal.open({
title: 'Neue Nachricht',
body: `${items}
`,
footer: ``,
});
document.getElementById('chat-picker-cancel')?.addEventListener('click', UI.modal.close);
document.querySelectorAll('[data-uid]').forEach(btn => {
btn.addEventListener('click', async () => {
UI.modal.close();
const uid = parseInt(btn.dataset.uid);
try {
const { conversation_id } = await API.chat.start(uid);
await _openThread(conversation_id);
} catch (e) { UI.toast.error(e.message); }
});
});
}
// ----------------------------------------------------------
return {
init,
_showList,
_openThread,
_send,
_deleteMsg,
_onPhotoSelected,
};
})();