WIP: новая схема сообщений и push (не проверено)

This commit is contained in:
AidarKC
2026-04-19 20:41:58 +03:00
parent f0b560ec06
commit cc59bd18ee
27 changed files with 1668 additions and 94 deletions
+110 -5
View File
@@ -1,20 +1,59 @@
import { renderHeader } from '../components/header.js';
import { directMessages } from '../mock-data.js';
import { addAppLogEntry, addChatMessage, getChatMessages, authService, state } from '../state.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}`;
bubble.textContent = msg.text;
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 }) {
@@ -27,6 +66,7 @@ export function render({ navigate, route }) {
const screen = document.createElement('section');
screen.className = 'stack';
const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase());
screen.append(
renderHeader({
@@ -60,6 +100,37 @@ export function render({ navigate, route }) {
})
);
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';
@@ -79,7 +150,7 @@ export function render({ navigate, route }) {
const text = input.value.trim();
if (!text) return;
addChatMessage(chatId, text);
const tempId = addOutgoingPendingMessage(chatId, text);
input.value = '';
renderLog(log, chatId);
@@ -90,16 +161,20 @@ export function render({ navigate, route }) {
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?.messageId || '',
messageId: result?.outgoingKey || '',
deliveredWsSessions: Number(result?.deliveredWsSessions || 0),
deliveredWebPushSessions: Number(result?.deliveredWebPushSessions || 0),
sessionNotFound: Boolean(result?.sessionNotFound),
},
});
} catch (e) {
@@ -118,7 +193,37 @@ export function render({ navigate, route }) {
});
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' },
});
}
}
}
+40 -14
View File
@@ -1,6 +1,6 @@
import { renderHeader } from '../components/header.js';
import { directMessages } from '../mock-data.js';
import { getChatMessages } from '../state.js';
import { getChatMessages, setContacts, state } from '../state.js';
import { loadCurrentRelations } from '../services/user-connections.js';
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
@@ -30,6 +30,7 @@ export function render({ navigate }) {
<div>
<div class="row" style="justify-content:flex-start; gap:8px;">
<strong>${item.name}</strong>
${item.notInContacts ? '<span class="meta-muted">не в контактах</span>' : ''}
</div>
<p class="meta-muted" style="margin-top:4px;">${item.lastMessage}</p>
</div>
@@ -46,32 +47,57 @@ export function render({ navigate }) {
try {
const relations = await loadCurrentRelations();
const contacts = relations.outContacts || [];
setContacts(contacts);
list.innerHTML = '';
if (!contacts.length) {
const empty = document.createElement('div');
empty.className = 'card meta-muted';
empty.textContent = 'Ваш список контактов пока пуст';
list.append(empty);
status.className = 'status-line is-available';
status.textContent = 'Нет контактов.';
return;
}
const rows = contacts.map((login) => {
const contactRows = contacts.map((login) => {
const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase());
const chat = getChatMessages(login);
const lastChat = chat[chat.length - 1];
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
return {
id: login,
initials: (login[0] || '?').toUpperCase(),
name: preview?.name || login,
lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.',
time: preview?.time || '—',
unread: Number(preview?.unread || 0),
unread,
notInContacts: false,
};
});
const allChatIds = Object.keys(state.chats || {})
.filter((id) => id && id.toLowerCase() !== String(state.session.login || '').toLowerCase())
.filter((id) => (getChatMessages(id) || []).length > 0);
const contactKeys = new Set(contacts.map((x) => String(x || '').toLowerCase()));
const extraRows = allChatIds
.filter((login) => !contactKeys.has(String(login || '').toLowerCase()))
.map((login) => {
const chat = getChatMessages(login);
const lastChat = chat[chat.length - 1];
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
return {
id: login,
initials: (login[0] || '?').toUpperCase(),
name: login,
lastMessage: lastChat?.text || 'Диалог пока пуст.',
time: 'сейчас',
unread,
notInContacts: true,
};
});
const rows = [...contactRows, ...extraRows];
if (!rows.length) {
const empty = document.createElement('div');
empty.className = 'card meta-muted';
empty.textContent = 'Пока нет ни контактов, ни сообщений';
list.append(empty);
status.className = 'status-line is-available';
status.textContent = 'Нет диалогов.';
return;
}
rows.forEach((item) => list.append(renderRow(item)));
status.className = 'status-line is-available';
status.textContent = `Загружено диалогов: ${rows.length}`;