import { directMessages } from '../mock-data.js';
import { state } from '../state.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { loadProfileSnapshot } from '../services/user-profile-params.js';
import { resolveDmVisualState } from './messages/dm-visual-resolver.js';
import { makeProfileRoute } from '../services/shine-routes.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;
}
// Аватар: реальное фото через renderUserAvatar (lazy), при отсутствии — аккуратные инициалы.
// Ошибка загрузки снапшота не ломает карточку (catch → инициалы остаются).
function createDmAvatar(login, { upgrade = true, name = '', photo = '' } = {}) {
const cleanLogin = String(login || '').trim();
const title = cleanLogin ? `Профиль ${cleanLogin}` : '';
// Инициалы для fallback берём из имени диалога («Марина К.» → «МК»), а не из служебного id.
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
const firstName = parts[0] || '';
const lastName = parts[1] || '';
const avatarEl = renderUserAvatar({ login: cleanLogin || 'unknown', firstName, lastName, size: 'small', title });
// Тестовое фото (demo, «как в Связях»): pravatar поверх инициалов через .has-image; офлайн/ошибка → инициалы.
const photoUrl = String(photo || '').trim();
if (photoUrl) {
const img = document.createElement('img');
// eager (не lazy): аватары у верха списка; lazy в некоторых движках/headless не догружает фото.
img.alt = ''; img.loading = 'eager'; img.decoding = 'async';
img.addEventListener('load', () => avatarEl.classList.add('has-image'));
img.addEventListener('error', () => { try { img.remove(); } catch (e) { /* остаются инициалы */ } avatarEl.classList.remove('has-image'); });
img.src = photoUrl;
avatarEl.append(img);
}
// upgrade=false (demo/lab) → остаёмся на тестовом фото/инициалах, без сетевого запроса профиля.
if (!cleanLogin || !upgrade) return avatarEl;
void loadDmAvatarSnapshot(cleanLogin).then((snapshot) => {
if (!avatarEl.isConnected) return;
const upgraded = renderUserAvatar({
login: cleanLogin,
firstName, lastName,
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;
}
// Иконки: галочка-подтверждён (gold, БЕЗ текста), цепочка (значок «связь через кого»), шеврон.
const SVG_CHECK = '';
const SVG_LINK = '';
const SVG_CHEVRON = '';
export function render({ navigate, route }) {
const screen = document.createElement('section');
screen.className = 'dm-screen dm-list-screen';
// Слева сверху — имя владельца аккаунта (реальный логин из сессии).
const login = String(state.session.login || '').trim();
// DM-шапка: grid 1fr auto 1fr (бренд слева, title строго по центру, «+» справа).
const head = document.createElement('header');
head.className = 'dm-head';
head.innerHTML = `
${(login[0] || 'A').toUpperCase()}
${login}
Shine
`;
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 = 'dm-list';
function renderRow(item) {
const v = resolveDmVisualState(item); // { tone, shining, confirmed, via, unread }
const cardVariant = v.tone === 'family' ? ' dm-card--family' : (v.tone === 'shining' ? ' dm-card--shining' : '');
const name = item.name || item.id;
const preview = (item.preview || item.lastMessage || '') || 'Диалог пока пуст.';
const row = document.createElement('article');
row.className = `dm-dialog-card${cardVariant}`;
row.tabIndex = 0;
row.setAttribute('role', 'button');
// Галочка-подтверждён — у имени, БЕЗ слова «Подтверждён».
const checkHtml = v.confirmed ? `${SVG_CHECK}` : '';
const unreadHtml = v.unread ? `${v.unread.label}` : '';
row.innerHTML = `
${name}
${checkHtml}
${preview}
${unreadHtml}${SVG_CHEVRON}
`;
// Значок «связь через кого» (вместо слова «Связь»): цепочка + мини-аватар первого посредника, сразу за галочкой.
// Тап (stopPropagation) → попап пути «Ты → … → он»; сама карточка по-прежнему открывает чат.
if (v.via && v.via.length) {
const titleline = row.querySelector('.dm-row-titleline');
const viaBtn = document.createElement('button');
viaBtn.type = 'button';
viaBtn.className = 'dm-via';
viaBtn.setAttribute('aria-label', `Связь через ${v.via.map((x) => x.name).join(', ')}`);
viaBtn.innerHTML = `${SVG_LINK}`; // только иконка (без мини-аватара/«+N»)
titleline.appendChild(viaBtn);
// Попап пути: Ты → …посредники… → целевой. Каждый узел — фото-аватар + имя.
// Тап по человеку (кроме «Ты») → его профиль (makeProfileRoute), чтобы отследить цепочку.
const pop = document.createElement('div');
pop.className = 'dm-via-path';
const chain = [
{ name: 'Ты', me: true },
...v.via.map((x) => ({ name: x.name, login: x.login || '', photo: x.photo || '' })),
{ name, login: item.login || item.id, photo: item.photo || '' },
];
chain.forEach((node, i) => {
if (i) {
const arr = document.createElement('span');
arr.className = 'dm-via-arrow';
arr.textContent = '→';
pop.appendChild(arr);
}
const clickable = !node.me && Boolean(node.login);
const el = document.createElement(clickable ? 'button' : 'span');
el.className = 'dm-via-node';
const ava = document.createElement('span');
ava.className = 'dm-via-node-ava';
if (node.me) {
const me = document.createElement('span');
me.className = 'dm-via-me';
me.textContent = (login[0] || 'A').toUpperCase();
ava.appendChild(me);
} else {
ava.appendChild(createDmAvatar(node.login || node.name, { upgrade: false, name: node.name, photo: node.photo }));
}
const nm = document.createElement('span');
nm.className = 'dm-via-node-name';
nm.textContent = node.name;
el.append(ava, nm);
if (clickable) {
el.type = 'button';
el.addEventListener('click', (e) => { e.stopPropagation(); navigate(makeProfileRoute(node.login)); });
}
pop.appendChild(el);
});
row.appendChild(pop);
const toggle = (e) => { e.stopPropagation(); pop.classList.toggle('is-open'); };
viaBtn.addEventListener('click', toggle);
viaBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e); }
});
}
// Аватар: фото/инициалы без кольца; у сияющего — свечение (класс is-shine).
const avWrap = document.createElement('div');
avWrap.className = `dm-av dm-av--${v.tone}${v.shining ? ' is-shine' : ''}`;
const avatarEl = createDmAvatar(item.id, { upgrade: true, name });
avatarEl.classList.add('avatar');
avWrap.appendChild(avatarEl);
row.prepend(avWrap);
const go = () => navigate(`chat-view/${encodeURIComponent(item.id)}`);
row.addEventListener('click', go);
row.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); }
});
return row;
}
// Источник списка — мок directMessages (плейсхолдер). На проде заменяется реальными
// relations/chats (relationFlagsForTarget/shineConfirmed/shine) — карточки и резолвер не меняются.
const items = Array.isArray(directMessages) ? directMessages : [];
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'card meta-muted';
empty.textContent = 'Пока нет диалогов';
list.append(empty);
} else {
items.forEach((item) => list.append(renderRow(item)));
}
screen.append(head, divider, list);
return screen;
}