UI: вернуть старую вкладку Личные и починить аватары в Связях
This commit is contained in:
parent
55e6e477be
commit
65fad993ad
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.220
|
client.version=1.2.221
|
||||||
server.version=1.2.208
|
server.version=1.2.209
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
|
import { renderHeader } from '../components/header.js';
|
||||||
import { directMessages } from '../mock-data.js';
|
import { directMessages } from '../mock-data.js';
|
||||||
import { state } from '../state.js';
|
import {
|
||||||
|
getChatMessages,
|
||||||
|
isSessionInvalidError,
|
||||||
|
setContacts,
|
||||||
|
state,
|
||||||
|
terminateCurrentSession,
|
||||||
|
} from '../state.js';
|
||||||
|
import { loadCurrentRelations } from '../services/user-connections.js';
|
||||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||||
import { loadProfileSnapshot } from '../services/user-profile-params.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: 'Личные сообщения' };
|
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
|
||||||
const dmAvatarSnapshotCache = new Map();
|
const dmAvatarSnapshotCache = new Map();
|
||||||
@ -30,36 +36,24 @@ async function loadDmAvatarSnapshot(login) {
|
|||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Аватар: реальное фото через renderUserAvatar (lazy), при отсутствии — аккуратные инициалы.
|
function createDmAvatar(login) {
|
||||||
// Ошибка загрузки снапшота не ломает карточку (catch → инициалы остаются).
|
|
||||||
function createDmAvatar(login, { upgrade = true, name = '', photo = '' } = {}) {
|
|
||||||
const cleanLogin = String(login || '').trim();
|
const cleanLogin = String(login || '').trim();
|
||||||
const title = cleanLogin ? `Профиль ${cleanLogin}` : '';
|
const title = cleanLogin ? `Профиль ${cleanLogin}` : '';
|
||||||
// Инициалы для fallback берём из имени диалога («Марина К.» → «МК»), а не из служебного id.
|
const avatarEl = renderUserAvatar({
|
||||||
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
|
login: cleanLogin || 'unknown',
|
||||||
const firstName = parts[0] || '';
|
size: 'small',
|
||||||
const lastName = parts[1] || '';
|
title,
|
||||||
const avatarEl = renderUserAvatar({ login: cleanLogin || 'unknown', firstName, lastName, size: 'small', title });
|
});
|
||||||
// Тестовое фото (demo, «как в Связях»): pravatar поверх инициалов через .has-image; офлайн/ошибка → инициалы.
|
if (!cleanLogin) return avatarEl;
|
||||||
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) => {
|
void loadDmAvatarSnapshot(cleanLogin).then((snapshot) => {
|
||||||
if (!avatarEl.isConnected) return;
|
if (!avatarEl.isConnected) return;
|
||||||
const upgraded = renderUserAvatar({
|
const upgraded = renderUserAvatar({
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
firstName, lastName,
|
|
||||||
avatar: snapshot?.avatar?.txId
|
avatar: snapshot?.avatar?.txId
|
||||||
? { ar: String(snapshot.avatar.txId || '').trim(), sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase() }
|
? {
|
||||||
|
ar: String(snapshot.avatar.txId || '').trim(),
|
||||||
|
sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(),
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
size: 'small',
|
size: 'small',
|
||||||
title,
|
title,
|
||||||
@ -70,151 +64,149 @@ function createDmAvatar(login, { upgrade = true, name = '', photo = '' } = {}) {
|
|||||||
return avatarEl;
|
return avatarEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иконки: галочка-подтверждён (gold, БЕЗ текста), цепочка (значок «связь через кого»), шеврон.
|
function formatChatRowTime(ts) {
|
||||||
const SVG_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.4 12.4l2.5 2.5 4.7-5.1"/></svg>';
|
const value = Number(ts || 0);
|
||||||
const SVG_LINK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 13a4 4 0 0 0 5.66 0l1.84-1.84a4 4 0 1 0-5.66-5.66l-1 1"/><path d="M13.5 11a4 4 0 0 0-5.66 0L6 12.84a4 4 0 1 0 5.66 5.66l1-1"/></svg>';
|
if (!Number.isFinite(value) || value <= 0) return '-';
|
||||||
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>';
|
return new Intl.DateTimeFormat('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(new Date(value));
|
||||||
|
}
|
||||||
|
|
||||||
export function render({ navigate, route }) {
|
export function render({ navigate }) {
|
||||||
const screen = document.createElement('section');
|
const screen = document.createElement('section');
|
||||||
screen.className = 'dm-screen dm-list-screen';
|
screen.className = 'stack dm-screen dm-list-screen';
|
||||||
|
|
||||||
// Слева сверху — имя владельца аккаунта (реальный логин из сессии).
|
screen.append(
|
||||||
const login = String(state.session.login || '').trim();
|
renderHeader({
|
||||||
|
title: 'Личные сообщения',
|
||||||
// DM-шапка: grid 1fr auto 1fr (бренд слева, title строго по центру, «+» справа).
|
leftLabel: String(state.session.login || '').trim(),
|
||||||
const head = document.createElement('header');
|
rightActions: [{ label: '+', onClick: () => navigate('contact-search-view') }],
|
||||||
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 dm-head-shine">Shine</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');
|
const list = document.createElement('div');
|
||||||
list.className = 'dm-list';
|
list.className = 'stack dm-list';
|
||||||
|
|
||||||
function renderRow(item) {
|
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');
|
const row = document.createElement('article');
|
||||||
row.className = `dm-dialog-card${cardVariant}`;
|
row.className = 'list-item dm-dialog-card';
|
||||||
row.tabIndex = 0;
|
const avatarEl = createDmAvatar(item.id);
|
||||||
row.setAttribute('role', 'button');
|
avatarEl.classList.add('avatar');
|
||||||
|
|
||||||
// Галочка-подтверждён — у имени, БЕЗ слова «Подтверждён».
|
|
||||||
const checkHtml = v.confirmed ? `<span class="dm-name-check" title="Подтверждён" aria-label="Подтверждён">${SVG_CHECK}</span>` : '';
|
|
||||||
const unreadHtml = v.unread ? `<span class="dm-unread-badge">${v.unread.label}</span>` : '';
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="dm-row-main">
|
<div class="dm-row-main">
|
||||||
<div class="dm-row-titleline">
|
<div class="dm-row-title-wrap">
|
||||||
<strong class="dm-row-title">${name}</strong>
|
<strong class="dm-row-title">${item.name}</strong>
|
||||||
${checkHtml}
|
${item.notInContacts ? '<span class="meta-muted">не в контактах</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<p class="dm-row-last-message">${preview}</p>
|
<p class="meta-muted dm-row-last-message">${item.lastMessage}</p>
|
||||||
|
</div>
|
||||||
|
<div class="dm-row-meta-col">
|
||||||
|
${item.unread ? `<span class="unread">${item.unread}</span>` : '<span></span>'}
|
||||||
|
<span class="meta-muted dm-row-time">${item.time}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dm-row-meta">${unreadHtml}<span class="dm-chevron">${SVG_CHEVRON}</span></div>
|
|
||||||
`;
|
`;
|
||||||
|
row.prepend(avatarEl);
|
||||||
// Значок «связь через кого» (вместо слова «Связь»): цепочка + мини-аватар первого посредника, сразу за галочкой.
|
row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(item.id)}`));
|
||||||
// Тап (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 = `<span class="dm-via-icon">${SVG_LINK}</span>`; // только иконка (без мини-аватара/«+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;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Источник списка — мок directMessages (плейсхолдер). На проде заменяется реальными
|
async function loadList() {
|
||||||
// relations/chats (relationFlagsForTarget/shineConfirmed/shine) — карточки и резолвер не меняются.
|
try {
|
||||||
const items = Array.isArray(directMessages) ? directMessages : [];
|
const relations = await loadCurrentRelations();
|
||||||
if (!items.length) {
|
const contacts = relations.outContacts || [];
|
||||||
const empty = document.createElement('div');
|
setContacts(contacts);
|
||||||
empty.className = 'card meta-muted';
|
list.innerHTML = '';
|
||||||
empty.textContent = 'Пока нет диалогов';
|
|
||||||
list.append(empty);
|
const contactRows = contacts.map((login) => {
|
||||||
} else {
|
const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase());
|
||||||
items.forEach((item) => list.append(renderRow(item)));
|
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: 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);
|
screen.append(list);
|
||||||
|
loadList();
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
// model = { focusId, nodes: [{ id, login, name, avatar, relationType, strength, shining, tier }] }
|
// model = { focusId, nodes: [{ id, login, name, avatar, relationType, strength, shining, tier }] }
|
||||||
|
|
||||||
import { renderUserAvatar, buildAvatarInitials } from '../../components/avatar-image.js';
|
import { renderUserAvatar, buildAvatarInitials } from '../../components/avatar-image.js';
|
||||||
|
import { state } from '../../state.js';
|
||||||
|
import { buildArweaveDataUrl } from '../../services/arweave-file-service.js';
|
||||||
|
|
||||||
const SVGNS = 'http://www.w3.org/2000/svg';
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
@ -105,6 +107,26 @@ function relationColor(relationType) {
|
|||||||
return RELATION_COLORS[relationType] || RELATION_COLORS.contact;
|
return RELATION_COLORS[relationType] || RELATION_COLORS.contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAvatarPhotoSrc(src) {
|
||||||
|
const directPhoto = String(src?.photo || '').trim();
|
||||||
|
if (directPhoto) return directPhoto;
|
||||||
|
|
||||||
|
const rawAvatar = src?.avatar;
|
||||||
|
if (!rawAvatar || rawAvatar === 'url_to_image') return null;
|
||||||
|
if (typeof rawAvatar === 'string') return String(rawAvatar).trim() || null;
|
||||||
|
|
||||||
|
const txId = String(rawAvatar?.ar || '').trim();
|
||||||
|
if (!txId) return null;
|
||||||
|
try {
|
||||||
|
return buildArweaveDataUrl({
|
||||||
|
gateway: state?.entrySettings?.arweaveServer,
|
||||||
|
txId,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Однократно внедряем скрытый SVG-фильтр свечения «сияющих» узлов (мягкое гауссово размытие).
|
// Однократно внедряем скрытый SVG-фильтр свечения «сияющих» узлов (мягкое гауссово размытие).
|
||||||
// Применяется к псевдо-ореолу ::before, а НЕ к самой аватарке — поэтому фото не размывается.
|
// Применяется к псевдо-ореолу ::before, а НЕ к самой аватарке — поэтому фото не размывается.
|
||||||
function ensureShineFilter() {
|
function ensureShineFilter() {
|
||||||
@ -487,7 +509,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
|
|
||||||
// Аватар = SVG-«стеклянный орб» (фото в стеклянной сфере). Хост — .node-dot (масштаб/состояния/
|
// Аватар = SVG-«стеклянный орб» (фото в стеклянной сфере). Хост — .node-dot (масштаб/состояния/
|
||||||
// синхронизация позиций движка). Меняем ТОЛЬКО аватар; бейдж/имя/линии — как раньше.
|
// синхронизация позиций движка). Меняем ТОЛЬКО аватар; бейдж/имя/линии — как раньше.
|
||||||
const photoSrc = src.photo || (src.avatar && src.avatar !== 'url_to_image' ? src.avatar : null);
|
const photoSrc = resolveAvatarPhotoSrc(src);
|
||||||
const initials = buildAvatarInitials({ login: src.login || src.name || String(src.id), firstName: src.name || '' });
|
const initials = buildAvatarInitials({ login: src.login || src.name || String(src.id), firstName: src.name || '' });
|
||||||
const dot = document.createElement('div');
|
const dot = document.createElement('div');
|
||||||
dot.className = 'avatar node-dot fg-orb-host';
|
dot.className = 'avatar node-dot fg-orb-host';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user