SHA256
232 lines
8.4 KiB
JavaScript
232 lines
8.4 KiB
JavaScript
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;
|
|
}
|