// Single chat view const ChatView = { chatId: null, otherId: null, otherName: null, ws: null, async render(root, params) { this.chatId = params.chatId; this.otherId = params.otherId; this.otherName = params.otherName; root.innerHTML = `
${escapeHtml(((this.otherName || '?')[0] || '?').toUpperCase())}
${escapeHtml(this.otherName || '')}
`; document.getElementById('chat-back').onclick = () => App.showView('chats'); document.getElementById('chat-profile').onclick = () => App.openUserProfile(this.otherId, this.otherName); document.getElementById('chat-review').onclick = () => App.openReview(this.chatId, this.otherId, this.otherName); document.getElementById('msg-attach').onclick = () => { const inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.onchange = () => this.sendPhoto(inp.files[0]); inp.click(); }; document.getElementById('msg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send(); } }); document.getElementById('msg-send').onclick = () => this.send(); this.loadMessages(); this.setupWS(); this.markRead(); }, async loadMessages() { try { const r = await API.ChatApi.listMessages(this.chatId); const box = document.getElementById('messages'); box.innerHTML = ''; r.messages.forEach((m) => this.appendMessage(m)); box.scrollTop = box.scrollHeight; } catch (e) { console.warn(e); } }, appendMessage(m) { const box = document.getElementById('messages'); const me = API.Store.user && m.sender_id === (API.Store.user.id || API.Store.user.user_id); const div = document.createElement('div'); div.className = 'bubble-b ' + (me ? 'out' : 'in'); div.dataset.id = m.id; if (m.deleted) { div.innerHTML = 'Удалено'; } else { const readMark = me ? (m.read_at ? '✓✓' : '✓') : ''; div.innerHTML = `
${escapeHtml(m.body || '')}
${formatTime(m.created_at)} ${m.edited ? 'ред.' : ''} ${readMark}
`; if (me) { div.onclick = () => this.onMessageClick(m); } } box.appendChild(div); box.scrollTop = box.scrollHeight; }, onMessageClick(m) { TG_APP.showConfirm('Удалить сообщение?').then((ok) => { if (ok) this.deleteMessage(m); }); }, async deleteMessage(m) { try { await API.ChatApi.deleteMessage(m.id); const el = document.querySelector(`.bubble[data-id="${m.id}"]`); if (el) el.innerHTML = 'Удалено'; } catch (e) { TG_APP.showAlert('Ошибка: ' + e.message); } }, setupWS() { if (!API.Store.access) return; const url = (window.API_BASE || 'https://api.buhapp.mygoodservice.ru').replace(/^http/, 'ws') + '/ws?token=' + API.Store.access; try { this.ws = new WebSocket(url); this.ws.onopen = () => console.log('WS connected'); this.ws.onmessage = (ev) => { try { const d = JSON.parse(ev.data); if (d.type === 'message' && d.payload.chat_id === this.chatId) { this.appendMessage(d.payload); } else if (d.type === 'message_edited' && d.payload.chat_id === this.chatId) { this.updateMessage(d.payload); } else if (d.type === 'message_read' && d.payload.chat_id === this.chatId) { this.markMessagesRead(); } } catch {} }; this.ws.onclose = () => setTimeout(() => this.setupWS(), 3000); this.ws.onerror = (e) => console.warn('WS error', e); } catch (e) { console.warn(e); } }, async markMessagesRead() { try { const r = await API.ChatApi.listMessages(this.chatId); r.messages.forEach((m) => { if (m.read_at) { const el = document.querySelector(`.bubble[data-id="${m.id}"] .read-tick`); if (el) { el.classList.add('read'); el.textContent = '✓✓'; } } }); } catch {} }, updateMessage(p) { const el = document.querySelector(`.bubble[data-id="${p.id}"]`); if (el) { el.querySelector('div:first-child') && (el.querySelector('div:first-child').textContent = p.body); const meta = el.querySelector('.meta'); if (meta && !meta.querySelector('.edited')) { const ed = document.createElement('span'); ed.className = 'edited'; ed.textContent = 'ред.'; meta.appendChild(ed); } } }, async send() { const inp = document.getElementById('msg-input'); const btn = document.getElementById('msg-send'); const body = inp.value.trim(); if (!body) { App._toast('Введите сообщение', 'err'); btn.classList.add('err'); setTimeout(() => btn.classList.remove('err'), 600); return; } btn.classList.add('load'); try { await API.ChatApi.sendMessage(this.chatId, body); inp.value = ''; btn.classList.remove('load'); } catch (e) { btn.classList.remove('load'); App._toast('⚠️ ' + (e.message || 'Не отправлено'), 'err'); } }, async sendPhoto(file) { if (!file) return; if (file.size > 8 * 1024 * 1024) { App._toast('Файл больше 8 МБ', 'err'); return; } App._toast('⏳ Загружаю фото…', 'ok'); const fd = new FormData(); fd.append('photo', file); try { const r = await fetch(API_BASE + '/api/v1/chats/' + this.chatId + '/photo', { method: 'POST', body: fd, credentials: 'include' }); if (!r.ok) throw new Error((await r.json()).error || 'HTTP ' + r.status); App._toast('✓ Фото отправлено', 'ok'); this.loadMessages(); } catch (e) { App._toast('⚠️ ' + (e.message || 'Ошибка'), 'err'); } }, async markRead() { try { await API.ChatApi.markRead(this.chatId); } catch {} }, cleanup() { if (this.ws) { this.ws.close(); this.ws = null; } }, }; window.ChatView = ChatView;