SHiNE-server/shine-UI/js/pages/messages-list.js
2026-06-21 12:27:41 +04:00

232 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { directMessages } from '../mock-data.js';
import {
getChatMessages,
isSessionInvalidError,
normalizeDmChatId,
setContacts,
state,
terminateCurrentSession,
} from '../state.js';
import { loadCurrentRelations } from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { loadProfileSnapshot } from '../services/user-profile-params.js';
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
const dmAvatarSnapshotCache = new Map();
const dmAvatarPendingByLogin = new Map();
async function loadDmAvatarSnapshot(login) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) return null;
const key = cleanLogin.toLowerCase();
if (dmAvatarSnapshotCache.has(key)) return dmAvatarSnapshotCache.get(key);
if (dmAvatarPendingByLogin.has(key)) return dmAvatarPendingByLogin.get(key);
const pending = loadProfileSnapshot(cleanLogin)
.then((snapshot) => {
dmAvatarSnapshotCache.set(key, snapshot || null);
dmAvatarPendingByLogin.delete(key);
return snapshot || null;
})
.catch(() => {
dmAvatarSnapshotCache.set(key, null);
dmAvatarPendingByLogin.delete(key);
return null;
});
dmAvatarPendingByLogin.set(key, pending);
return pending;
}
function createDmAvatar(login) {
const cleanLogin = String(login || '').trim();
const title = cleanLogin ? `Профиль ${cleanLogin}` : '';
const avatarEl = renderUserAvatar({
login: cleanLogin || 'unknown',
size: 'small',
title,
});
if (!cleanLogin) return avatarEl;
void loadDmAvatarSnapshot(cleanLogin).then((snapshot) => {
if (!avatarEl.isConnected) return;
const upgraded = renderUserAvatar({
login: cleanLogin,
avatar: snapshot?.avatar?.txId
? {
ar: String(snapshot.avatar.txId || '').trim(),
sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(),
}
: null,
size: 'small',
title,
});
upgraded.classList.add('avatar');
avatarEl.replaceWith(upgraded);
});
return avatarEl;
}
function formatChatRowTime(ts) {
const value = Number(ts || 0);
if (!Number.isFinite(value) || value <= 0) return '';
return new Intl.DateTimeFormat('ru-RU', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(value));
}
const SVG_CHEVRON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>';
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack dm-screen dm-list-screen';
const login = String(state.session.login || '').trim();
const head = document.createElement('header');
head.className = 'dm-head';
head.innerHTML = `
<div class="dm-head-brand">
<div class="dm-head-hex">${(login[0] || 'A').toUpperCase()}</div>
<div class="dm-head-id">
<span class="dm-head-name">${login}</span>
</div>
</div>
<h1 class="dm-head-title">Контакты</h1>
<button type="button" class="dm-head-plus" aria-label="Новый диалог">+</button>
`;
head.querySelector('.dm-head-plus')?.addEventListener('click', () => navigate('contact-search-view'));
const divider = document.createElement('div');
divider.className = 'dm-divider';
const list = document.createElement('div');
list.className = 'stack dm-list';
function renderRow(item) {
const row = document.createElement('article');
row.className = 'list-item dm-dialog-card';
const avatarEl = createDmAvatar(item.id);
avatarEl.classList.add('avatar');
const avatarWrap = document.createElement('div');
avatarWrap.className = 'dm-av dm-av--default';
avatarWrap.append(avatarEl);
row.innerHTML = `
<div class="dm-row-main">
<div class="dm-row-titleline dm-row-titlewrap">
<strong class="dm-row-title">${item.name}</strong>
${item.notInContacts ? '<span class="dm-contact-note">не в контактах</span>' : ''}
</div>
<p class="dm-row-last-message">${item.lastMessage}</p>
</div>
<div class="dm-row-meta-col">
${item.unread ? `<span class="dm-unread-badge">${item.unread > 99 ? '99+' : item.unread}</span>` : '<span class="dm-row-meta-spacer" aria-hidden="true"></span>'}
<div class="dm-row-meta-line">
${item.time ? `<span class="dm-row-time">${item.time}</span>` : '<span class="dm-row-time dm-row-time--empty"></span>'}
<span class="dm-chevron">${SVG_CHEVRON}</span>
</div>
</div>
`;
row.prepend(avatarWrap);
row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(normalizeDmChatId(item.id))}`));
return row;
}
async function loadList() {
try {
const relations = await loadCurrentRelations();
const contacts = relations.outContacts || [];
setContacts(contacts);
list.innerHTML = '';
const contactRows = contacts.map((login) => {
const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase());
const canonicalLogin = normalizeDmChatId(login);
const chat = getChatMessages(canonicalLogin);
const lastChat = chat[chat.length - 1];
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
const lastTimeMs = Number(lastChat?.createdAtMs || 0);
return {
id: canonicalLogin,
name: preview?.name || login,
lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.',
time: formatChatRowTime(lastTimeMs),
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;
const lastTimeMs = Number(lastChat?.createdAtMs || 0);
return {
id: login,
name: login,
lastMessage: lastChat?.text || 'Диалог пока пуст.',
time: formatChatRowTime(lastTimeMs),
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);
return;
}
rows.forEach((item) => list.append(renderRow(item)));
} catch (error) {
if (isSessionInvalidError(error)) {
list.innerHTML = '';
const card = document.createElement('div');
card.className = 'card stack';
const title = document.createElement('strong');
title.textContent = 'Сессия устарела';
const details = document.createElement('p');
details.className = 'meta-muted';
details.textContent = 'Ваша сессия больше не действует. Авторизуйтесь заново.';
const okBtn = document.createElement('button');
okBtn.type = 'button';
okBtn.className = 'primary-btn';
okBtn.textContent = 'ОК';
okBtn.addEventListener('click', async () => {
await terminateCurrentSession({
infoMessage: 'Ваша сессия устарела. Выполните вход заново.',
});
navigate('start-view');
});
card.append(title, details, okBtn);
list.append(card);
return;
}
list.innerHTML = '';
const fail = document.createElement('div');
fail.className = 'card meta-muted';
fail.textContent = `Не удалось загрузить сообщения: ${error.message || 'unknown'}`;
list.append(fail);
}
}
screen.append(head, divider, list);
loadList();
return screen;
}