/* ============================================================ 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; // Delegierter Click-Handler — Inline-onclick wird von der CSP blockiert. if (!_container._chatClickBound) { _container.addEventListener('click', e => { const t = e.target.closest('[data-chat-action]'); if (!t) return; switch (t.dataset.chatAction) { case 'open': _openThread(parseInt(t.dataset.chatId, 10)); break; case 'list': _showList(); break; case 'photo': document.getElementById('chat-photo-input')?.click(); break; case 'send': _send(); break; case 'delete': _deleteMsg(parseInt(t.dataset.chatId, 10)); break; case 'img': window.open(t.dataset.chatUrl, '_blank'); break; } }); _container._chatClickBound = true; } // 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 // ---------------------------------------------------------- const _isDesktop = () => window.innerWidth >= 768; function _listPaneHTML() { return `

Nachrichten

`; } async function _showList() { _view = 'list'; _stopPolling(); _convId = null; if (_isDesktop()) { // Split-Pane: linke Spalte bleibt, rechte zeigt Placeholder if (!document.getElementById('chat-split')) { _container.innerHTML = `
${_listPaneHTML()}
${UI.icon('chat-circle-dots')} Gespräch auswählen
`; document.getElementById('chat-new-btn')?.addEventListener('click', _showNewMessagePicker); } else { // Split existiert — nur rechte Seite zurücksetzen const pane = document.getElementById('chat-thread-pane'); if (pane) { pane.style.cssText = 'flex:1;display:flex;align-items:center;justify-content:center;background:var(--c-bg);color:var(--c-text-muted);font-size:var(--text-sm)'; pane.innerHTML = `${UI.icon('chat-circle-dots')} Gespräch auswählen`; } } } else { _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 ? UI.escape(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 ? `` : ''}
${UI.escape(c.partner_name)}
${preview}
${timeStr} ${badge}
`; }).join(''); } catch (e) { if (e.status === 401) { document.getElementById('chat-list-body').innerHTML = `

Bitte melde dich an.

`; } } } // ---------------------------------------------------------- // Thread (message view) // ---------------------------------------------------------- async function _openThread(convId) { _convId = convId; _view = 'thread'; _stopPolling(); // Aktive Markierung in der Liste document.querySelectorAll('.chat-conv-item').forEach(el => el.classList.toggle('active', el.dataset.chatId === String(convId)) ); const threadHTML = `
${_isDesktop() ? '' : ` `}
?
Laden…
`; const threadPane = document.getElementById('chat-thread-pane'); if (_isDesktop() && threadPane) { threadPane.style.cssText = 'flex:1;min-width:0;display:flex;flex-direction:column'; threadPane.innerHTML = threadHTML; } else { _container.innerHTML = threadHTML; } // 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(); _send(); } }); document.getElementById('chat-photo-input') ?.addEventListener('change', e => _onPhotoSelected(e.target)); 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.classList.toggle('hidden', !data.partner_online); 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 = `

Fehler beim Laden.

`; } } 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.classList.toggle('hidden', !data.partner_online); 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 += `Foto`; } if (m.text) { bubbleContent += (m.media_url ? `
` : '') + UI.escape(m.text) + (m.media_url ? `
` : ''); } if (!bubbleContent) bubbleContent = UI.escape(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' }); } // ---------------------------------------------------------- // 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); } }); }); } async function refresh() { await _loadList(); await _updateChatBadge(); } // ---------------------------------------------------------- return { init, refresh, _showList, _openThread, _send, _deleteMsg, _onPhotoSelected, }; })();