SHA256
Аватары: общий компонент, кэш txId и avatar.ar в графе связей
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user