Аватары: общий компонент, кэш txId и avatar.ar в графе связей

This commit is contained in:
AidarKC
2026-04-26 02:27:41 +03:00
parent 667c5310bf
commit 4c1aeeeac8
13 changed files with 392 additions and 103 deletions
+27 -6
View File
@@ -1,5 +1,6 @@
import { renderHeader } from '../components/header.js';
import { authService, state } from '../state.js';
import { renderUserAvatar } from '../components/avatar-image.js';
export const pageMeta = { id: 'network-view', title: 'Связи' };
@@ -56,11 +57,20 @@ function getMarkByLogin(allUsers) {
shine: Boolean(row?.shine),
officialLabel: String(row?.officialLabel || (row?.official ? 'официальный' : 'неофициальный')),
shineLabel: String(row?.shineLabel || (row?.shine ? 'сияющий' : 'несияющий')),
avatar: normalizeAvatar(row),
});
});
return map;
}
function normalizeAvatar(row) {
const txFromAvatar = String(row?.avatar?.ar || '').trim();
if (txFromAvatar) return { ar: txFromAvatar };
const txFallback = String(row?.avatarTxId || '').trim();
if (txFallback) return { ar: txFallback };
return null;
}
function applyRelativeGender(map, rows) {
(Array.isArray(rows) ? rows : []).forEach((row) => {
const login = normalizeLogin(row?.login);
@@ -108,12 +118,23 @@ function buildNodeElement({ login, kind = 'friend', isCenter = false, mark = nul
const metaText = metaParts.join(', ');
node.title = metaText ? `${login}\n${metaText}` : login;
const officialBadge = mark?.official ? '<span class="node-badge-official" aria-hidden="true">ОФ</span>' : '';
node.innerHTML = `
${officialBadge}
<span class="node-dot">${(login[0] || '?').toUpperCase()}</span>
<span class="node-label">${login}</span>
`;
if (mark?.official) {
const officialBadge = document.createElement('span');
officialBadge.className = 'node-badge-official';
officialBadge.setAttribute('aria-hidden', 'true');
officialBadge.textContent = 'ОФ';
node.append(officialBadge);
}
node.append(renderUserAvatar({
login,
avatar: mark?.avatar || null,
size: 'node',
title: login,
}));
const label = document.createElement('span');
label.className = 'node-label';
label.textContent = login;
node.append(label);
return node;
}
+15 -56
View File
@@ -11,8 +11,8 @@ import {
saveProfileToggle,
} from '../services/user-profile-params.js';
import { buildIdentityLines, loadUserProfileCard } from '../services/user-connections.js';
import { buildArweaveDataUrl } from '../services/arweave-file-service.js';
import { openAvatarWizard } from '../components/avatar-wizard.js';
import { renderUserAvatar } from '../components/avatar-image.js';
export const pageMeta = { id: 'profile-view', title: 'Профиль' };
@@ -105,10 +105,7 @@ export function render({ navigate }) {
topRow.innerHTML = `
<div class="row" style="gap:12px; align-items:center;">
<div class="profile-avatar-block">
<div class="avatar large profile-avatar" data-profile-avatar="true">
<span data-avatar-fallback="true">${profile.avatarInitials}</span>
<img src="" alt="Аватар" data-avatar-image="true" hidden />
</div>
<div data-profile-avatar-slot="true"></div>
<button class="ghost-btn profile-avatar-change-btn" type="button" data-change-avatar="true">Сменить аватар</button>
</div>
<div class="profile-identity-lines" data-profile-identity="true">
@@ -154,8 +151,7 @@ export function render({ navigate }) {
let currentGender = PROFILE_GENDER_UNKNOWN;
let currentAvatar = { value: '', source: '', txId: '', timeMs: 0 };
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
const avatarImageEl = topRow.querySelector('[data-avatar-image="true"]');
const avatarFallbackEl = topRow.querySelector('[data-avatar-fallback="true"]');
const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]');
function openGenderPickerModal(initialGender) {
const root = document.getElementById('modal-root');
@@ -219,56 +215,19 @@ export function render({ navigate }) {
)).join('');
}
function buildAvatarFallbackText() {
function updateAvatarUi() {
if (!(avatarSlotEl instanceof HTMLElement)) return;
const firstName = String(currentFields.find((field) => field.key === 'first_name')?.value || '').trim();
const lastName = String(currentFields.find((field) => field.key === 'last_name')?.value || '').trim();
const fromNames = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
if (fromNames.trim()) return fromNames;
const loginFirst = String(login || '').trim().charAt(0).toUpperCase();
if (loginFirst) return loginFirst;
return String(profile.avatarInitials || '??').trim().slice(0, 2).toUpperCase();
}
function showAvatarFallback() {
if (avatarImageEl instanceof HTMLImageElement) {
avatarImageEl.hidden = true;
avatarImageEl.removeAttribute('src');
}
if (avatarFallbackEl instanceof HTMLElement) {
avatarFallbackEl.hidden = false;
avatarFallbackEl.textContent = buildAvatarFallbackText();
}
}
function updateAvatarUi() {
if (!(avatarImageEl instanceof HTMLImageElement)) return;
const txId = String(currentAvatar?.txId || '').trim();
if (!txId) {
showAvatarFallback();
return;
}
let avatarUrl = '';
try {
avatarUrl = buildArweaveDataUrl({
gateway: state.entrySettings.arweaveServer,
txId,
});
} catch {
showAvatarFallback();
return;
}
avatarImageEl.onload = () => {
if (avatarFallbackEl instanceof HTMLElement) avatarFallbackEl.hidden = true;
avatarImageEl.hidden = false;
};
avatarImageEl.onerror = () => {
showAvatarFallback();
};
avatarImageEl.src = avatarUrl;
avatarSlotEl.innerHTML = '';
avatarSlotEl.append(renderUserAvatar({
login,
firstName,
lastName,
avatar: currentAvatar?.txId ? { ar: currentAvatar.txId } : null,
size: 'large',
className: 'profile-avatar',
}));
}
function updateToggleButton(button, prefix, enabled) {
@@ -846,7 +805,7 @@ export function render({ navigate }) {
card.append(topRow, badgesRow, status, listWrap, relativesCard);
screen.append(card);
showAvatarFallback();
updateAvatarUi();
refreshProfileSnapshot();
return screen;
+30 -14
View File
@@ -1,11 +1,11 @@
import { renderHeader } from '../components/header.js';
import { authService, state } from '../state.js';
import {
buildAvatarInitials,
buildIdentityLines,
loadRelationsForPair,
loadUserProfileCard,
} from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
export const pageMeta = { id: 'user-profile-view', title: 'Чужой профиль' };
@@ -54,16 +54,31 @@ function renderIdentity(card) {
lastName: card.lastName,
});
return `
<div class="row" style="gap:12px; align-items:center;">
<div class="avatar large">${escapeHtml(buildAvatarInitials(card))}</div>
<div class="profile-identity-lines">
${lines.map((line, idx) => (
`<div class="profile-identity-line${idx === lines.length - 1 ? ' profile-identity-login' : ''}">${escapeHtml(line)}</div>`
)).join('')}
</div>
</div>
`;
const row = document.createElement('div');
row.className = 'row';
row.style.gap = '12px';
row.style.alignItems = 'center';
row.append(renderUserAvatar({
login: card.login,
firstName: card.firstName,
lastName: card.lastName,
avatar: card.avatar,
size: 'large',
className: 'profile-avatar',
}));
const identityLines = document.createElement('div');
identityLines.className = 'profile-identity-lines';
lines.forEach((line, idx) => {
const lineEl = document.createElement('div');
lineEl.className = `profile-identity-line${idx === lines.length - 1 ? ' profile-identity-login' : ''}`;
lineEl.textContent = line;
identityLines.append(lineEl);
});
row.append(identityLines);
return row;
}
function renderReadOnlyBadges(card) {
@@ -174,9 +189,6 @@ export function render({ navigate, route }) {
currentFlags = flags;
body.innerHTML = `
<div class="card stack">
${renderIdentity(card)}
</div>
${renderReadOnlyBadges(card)}
${renderRelations(flags)}
${renderReadOnlyParams(card)}
@@ -186,6 +198,10 @@ export function render({ navigate, route }) {
<button class="ghost-btn" type="button" data-relation-action="contact"></button>
</div>
`;
const identityCard = document.createElement('div');
identityCard.className = 'card stack';
identityCard.append(renderIdentity(card));
body.prepend(identityCard);
syncActionButtons();
status.className = 'status-line is-available';