import { renderHeader } from '../components/header.js'; import { directMessages } from '../mock-data.js'; import { addAppLogEntry, addChatMessage, addOutgoingPendingMessage, getChatMessages, markChatRead, markOutgoingSent, authService, state, } from '../state.js'; import { startOutgoingCall, hangupActiveCall } from '../services/call-service.js'; export const pageMeta = { id: 'chat-view', title: 'Чат' }; function parseBaseKey(baseKey) { const raw = String(baseKey || '').trim(); const parts = raw.split('|'); if (parts.length < 4) return null; const fromLogin = parts[0] || ''; const toLogin = parts[1] || ''; const timeMs = Number(parts[2] || 0); const nonce = Number(parts[3] || 0); if (!fromLogin || !toLogin || !Number.isFinite(timeMs) || !Number.isFinite(nonce)) return null; return { fromLogin, toLogin, timeMs, nonce }; } function renderLog(list, chatId) { list.innerHTML = ''; const messages = getChatMessages(chatId); let unreadSeparatorInserted = false; messages.forEach((msg) => { if (!unreadSeparatorInserted && msg?.from === 'in' && msg?.unread) { const sep = document.createElement('div'); sep.className = 'meta-muted'; sep.style.textAlign = 'center'; sep.style.margin = '8px 0'; sep.textContent = 'Новые сообщения'; list.append(sep); unreadSeparatorInserted = true; } const bubble = document.createElement('div'); bubble.className = `bubble ${msg.from}`; let text = msg.text || ''; if (msg.from === 'out') { if (msg.secondTick) text += ' ✓✓'; else if (msg.firstTick) text += ' ✓'; else text += ' …'; } bubble.textContent = text; list.append(bubble); }); list.scrollTop = list.scrollHeight; markChatRead(chatId); } export function render({ navigate, route }) { const chatId = route.params.chatId || 'u1'; const contact = directMessages.find((d) => d.id === chatId) || { id: chatId, name: chatId, initials: (chatId[0] || '?').toUpperCase(), }; const screen = document.createElement('section'); screen.className = 'stack'; const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase()); screen.append( renderHeader({ title: `Чат: ${contact.name}`, leftAction: { label: '←', onClick: () => navigate('messages-list') }, rightActions: [{ label: 'Позвонить', onClick: async () => { const confirmed = window.confirm('Позвонить этому пользователю?'); if (!confirmed) return; try { await startOutgoingCall(chatId); renderLog(log, chatId); } catch (e) { addChatMessage(chatId, `[call] Ошибка звонка: ${e.message || 'unknown'}`); renderLog(log, chatId); } }, }, { label: 'Сброс', onClick: async () => { try { await hangupActiveCall(); renderLog(log, chatId); } catch (e) { addChatMessage(chatId, `[call] Ошибка сброса: ${e.message || 'unknown'}`); renderLog(log, chatId); } }, }], }) ); if (!isKnownContact) { const card = document.createElement('div'); card.className = 'card'; const btn = document.createElement('button'); btn.className = 'secondary-btn'; btn.type = 'button'; btn.textContent = 'Добавить в контакты'; btn.addEventListener('click', async () => { try { await authService.addCloseFriend(chatId); state.contacts = [...new Set([...(state.contacts || []), chatId])]; addAppLogEntry({ level: 'info', source: 'contacts', message: `Пользователь ${chatId} добавлен в контакты`, }); btn.disabled = true; btn.textContent = 'Добавлено'; } catch (e) { addAppLogEntry({ level: 'warn', source: 'contacts', message: 'Не удалось добавить пользователя в контакты', details: { login: chatId, error: e?.message || 'unknown' }, }); } }); card.append(btn); screen.append(card); } const wrap = document.createElement('div'); wrap.className = 'chat-wrap'; const log = document.createElement('div'); log.className = 'messages-log'; const form = document.createElement('form'); form.className = 'chat-input'; form.innerHTML = ` `; form.addEventListener('submit', async (event) => { event.preventDefault(); const input = form.elements.message; const text = input.value.trim(); if (!text) return; const tempId = addOutgoingPendingMessage(chatId, text); input.value = ''; renderLog(log, chatId); try { const result = await authService.sendDirectMessage({ login: state.session.login, toLogin: chatId, text, storagePwd: state.session.storagePwdInMemory, }); markOutgoingSent(tempId, { messageKey: result?.outgoingKey || '', baseKey: result?.baseKey || result?.localBaseKey || '', }); renderLog(log, chatId); addAppLogEntry({ level: 'info', source: 'outgoing-dm', message: `Сообщение отправлено для ${chatId}`, details: { toLogin: chatId, messageId: result?.outgoingKey || '', deliveredWsSessions: Number(result?.deliveredWsSessions || 0), deliveredWebPushSessions: Number(result?.deliveredWebPushSessions || 0), }, }); } catch (e) { addChatMessage(chatId, `Ошибка отправки: ${e.message || 'unknown'}`); addAppLogEntry({ level: 'warn', source: 'outgoing-dm', message: 'Ошибка отправки личного сообщения', details: { toLogin: chatId, error: e?.message || 'unknown', }, }); renderLog(log, chatId); } }); renderLog(log, chatId); void sendReadReceiptsForVisible(); wrap.append(log, form); screen.append(wrap); return screen; } async function sendReadReceiptsForVisible() { const pending = getChatMessages(chatId) .filter((row) => row?.from === 'in' && Number(row?.messageType) === 1 && !row?.readReceiptSent) .slice(0, 50); for (const row of pending) { const ref = parseBaseKey(row.baseKey); if (!ref) continue; try { await authService.sendReadReceipt({ login: state.session.login, toLogin: ref.fromLogin, storagePwd: state.session.storagePwdInMemory, refToLogin: ref.toLogin, refFromLogin: ref.fromLogin, refTimeMs: ref.timeMs, refNonce: ref.nonce, refType: 1, }); row.readReceiptSent = true; } catch (e) { addAppLogEntry({ level: 'warn', source: 'read-receipt', message: 'Не удалось отправить подтверждение прочтения', details: { chatId, messageKey: row.messageKey || '', error: e?.message || 'unknown' }, }); } } }