SHA256
Доделать профиль, связи и серверные миграции
This commit is contained in:
@@ -91,6 +91,8 @@ import * as messagesList from './pages/messages-list.js?v=202608221218';
|
||||
import * as contactSearchView from './pages/contact-search-view.js';
|
||||
import * as chatView from './pages/chat-view.js?v=202608221218';
|
||||
import * as userProfileView from './pages/user-profile-view.js';
|
||||
import * as userProfileListView from './pages/user-profile-list-view.js';
|
||||
import * as userRelationManageView from './pages/user-relation-manage-view.js';
|
||||
import * as channelsList from './pages/channels-list.js?v=202608221218';
|
||||
import * as channelView from './pages/channel-view.js';
|
||||
import * as channelAboutView from './pages/channel-about-view.js';
|
||||
@@ -154,6 +156,8 @@ const routes = {
|
||||
'contact-search-view': contactSearchView,
|
||||
'chat-view': chatView,
|
||||
user: userProfileView,
|
||||
'user-profile-list-view': userProfileListView,
|
||||
'user-relation-manage-view': userRelationManageView,
|
||||
'channels-list': channelsList,
|
||||
'channel-view': channelView,
|
||||
'channel-about-view': channelAboutView,
|
||||
|
||||
@@ -775,6 +775,8 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
const versionsTotal = Number(node?.versionsTotal || versions.length || 1);
|
||||
const text = resolveNodeText(node) || (versionsTotal > 1 ? 'удалено' : '(пусто)');
|
||||
const likes = Number(node?.likesCount || 0);
|
||||
const primaryLikes = Number(node?.primaryLikesCount || 0);
|
||||
const shiningLikes = Number(node?.shiningLikesCount || 0);
|
||||
const replies = Number(node?.repliesCount || 0);
|
||||
const ratings = Number(node?.ratingsCount || 0);
|
||||
const isOwnMessage = String(node?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase();
|
||||
@@ -918,7 +920,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
likeButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">${isLiked ? '❤️' : '🤍'}</span>
|
||||
<span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
|
||||
<span class="channel-action-counter">${likes}</span>
|
||||
<span class="channel-action-counter" title="Все / основные / сияющие">${likes} · ${primaryLikes} · ${shiningLikes}</span>
|
||||
`;
|
||||
setActionTitle(likeButton, isPending ? 'Лайк...' : 'Лайк');
|
||||
likeButton.disabled = isPending;
|
||||
|
||||
@@ -1417,6 +1417,8 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
versionsTotal: Number(message?.versionsTotal || 1),
|
||||
versions: Array.isArray(message?.versions) ? message.versions : [],
|
||||
likesCount: Number(message?.likesCount || 0),
|
||||
primaryLikesCount: Number(message?.primaryLikesCount || 0),
|
||||
shiningLikesCount: Number(message?.shiningLikesCount || 0),
|
||||
repliesCount: Number(message?.repliesCount || 0),
|
||||
ratingsCount: Number(message?.ratingsCount || 0),
|
||||
timestampMs: resolveMessageTimestampMs(message),
|
||||
@@ -1961,7 +1963,7 @@ function renderPostCard(post, {
|
||||
likeButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">${isLiked ? '❤️' : '🤍'}</span>
|
||||
<span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
|
||||
<span class="channel-action-counter">${post.likesCount || 0}</span>
|
||||
<span class="channel-action-counter" title="Все / основные / сияющие">${post.likesCount || 0} · ${post.primaryLikesCount || 0} · ${post.shiningLikesCount || 0}</span>
|
||||
`;
|
||||
setActionTitle(likeButton, isPending ? 'Лайк...' : 'Лайк');
|
||||
likeButton.disabled = isPending;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||
import { authService } from '../state.js';
|
||||
import { navigateBack } from '../router.js';
|
||||
|
||||
export const pageMeta = { id: 'user-profile-list-view', title: 'Список' };
|
||||
|
||||
function parseAvatar(raw) {
|
||||
const value = String(raw || '');
|
||||
const m = value.match(/(?:^|,)\s*AR:([A-Za-z0-9_-]{43})(?:,|$)/);
|
||||
return m ? { ar: m[1] } : null;
|
||||
}
|
||||
|
||||
const TITLES = {
|
||||
friends: 'Друзья', close_friends: 'Близкие друзья', primary_received: 'Подтвердили основной аккаунт',
|
||||
primary_given: 'Подтверждённые аккаунты', shine_received: 'Считают сияющим', shine_given: 'Подтверждённые сияющие',
|
||||
channels_owned: 'Каналы', channels_following: 'Подписки на каналы',
|
||||
};
|
||||
|
||||
export function render({ navigate, route }) {
|
||||
const login = String(route?.params?.login || '').trim();
|
||||
const kind = String(route?.params?.kind || '').trim();
|
||||
const screen = document.createElement('section'); screen.className = 'stack';
|
||||
const body = document.createElement('div'); body.className = 'stack';
|
||||
const status = document.createElement('div'); status.className = 'status-line'; status.textContent = 'Загрузка...';
|
||||
screen.append(renderHeader({ title: TITLES[kind] || 'Список', leftAction: { label: '←', onClick: () => navigateBack() } }), status, body);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (kind === 'channels_owned' || kind === 'channels_following') {
|
||||
const payload = await authService.listUserProfileChannels(login, kind === 'channels_owned' ? 'owned' : 'following', 200, 0);
|
||||
const rows = Array.isArray(payload?.channels) ? payload.channels : [];
|
||||
rows.forEach((row) => {
|
||||
const el = document.createElement('button'); el.type = 'button'; el.className = 'card row profile-list-row';
|
||||
el.append(renderUserAvatar({ login: row.ownerLogin, firstName: row.displayName, lastName: '', avatar: parseAvatar(row.avatarAr), size: 'md' }));
|
||||
const t = document.createElement('div'); t.className = 'profile-list-row-text';
|
||||
t.innerHTML = `<b>${String(row.displayName || row.slug || '')}</b><small>${String(row.ownerLogin || '')} / ${String(row.slug || '')}</small>`;
|
||||
el.append(t);
|
||||
el.addEventListener('click', () => navigate(`channel/${encodeURIComponent(row.ownerBlockchainName)}/${Number(row.rootBlockNumber || 0)}/${encodeURIComponent(row.rootBlockHashHex || '')}/about`));
|
||||
body.append(el);
|
||||
});
|
||||
status.textContent = rows.length ? '' : 'Список пуст.';
|
||||
return;
|
||||
}
|
||||
const payload = await authService.listUserProfileRelations(login, kind, 200, 0);
|
||||
const rows = Array.isArray(payload?.users) ? payload.users : [];
|
||||
rows.forEach((row) => {
|
||||
const el = document.createElement('button'); el.type = 'button'; el.className = 'card row profile-list-row';
|
||||
el.append(renderUserAvatar({ login: row.login, firstName: row.firstName, lastName: row.lastName, avatar: parseAvatar(row.avatarAr), size: 'md' }));
|
||||
const fullName = [row.firstName, row.lastName].filter(Boolean).join(' ') || row.login;
|
||||
const t = document.createElement('div'); t.className = 'profile-list-row-text';
|
||||
const marks = [row.relationType && row.relationType !== 'none' ? row.relationType : '', row.primaryConfirmed ? 'основной ✓' : '', row.shineConfirmed ? 'сияющий ✓' : ''].filter(Boolean).join(' · ');
|
||||
t.innerHTML = `<b>${fullName}</b><small>${String(row.login || '')}${marks ? ` · ${marks}` : ''}</small>`;
|
||||
el.append(t); el.addEventListener('click', () => navigate(`SHiNE/${encodeURIComponent(row.login)}`)); body.append(el);
|
||||
});
|
||||
status.textContent = rows.length ? '' : 'Список пуст.';
|
||||
} catch (e) { status.className = 'status-line is-unavailable'; status.textContent = `Ошибка: ${e.message || 'unknown'}`; }
|
||||
})();
|
||||
return screen;
|
||||
}
|
||||
@@ -1,371 +1,56 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { authService, state } from '../state.js';
|
||||
import {
|
||||
buildIdentityLines,
|
||||
loadRelationsForPair,
|
||||
loadUserProfileCard,
|
||||
} from '../services/user-connections.js';
|
||||
import { state } from '../state.js';
|
||||
import { loadUserProfileCard } from '../services/user-connections.js';
|
||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
||||
|
||||
import { navigateBack } from '../router.js';
|
||||
|
||||
export const pageMeta = { id: 'user', title: 'Чужой профиль' };
|
||||
export const pageMeta = { id: 'user', title: 'Профиль пользователя' };
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
function escapeHtml(text){return String(text||'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",''');}
|
||||
function fullName(card){return [card.firstName,card.lastName].filter(Boolean).join(' ')||card.login;}
|
||||
|
||||
function openTextModal(title,text){
|
||||
const root=document.getElementById('modal-root'); if(!root)return;
|
||||
root.innerHTML=`<div class="modal" id="profile-text-modal"><div class="modal-card stack"><h3>${escapeHtml(title)}</h3><div style="white-space:pre-wrap;line-height:1.5">${escapeHtml(text||'Не заполнено')}</div><button class="secondary-btn" id="profile-text-close">Закрыть</button></div></div>`;
|
||||
const close=()=>{root.innerHTML='';}; root.querySelector('#profile-text-close')?.addEventListener('click',close); root.querySelector('#profile-text-modal')?.addEventListener('click',e=>{if(e.target?.id==='profile-text-modal')close();});
|
||||
}
|
||||
|
||||
function openProfileInfoModal({ title, text }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
if (!root) return;
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="profile-info-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">${escapeHtml(title)}</h3>
|
||||
<p class="meta-muted" style="white-space: pre-wrap; line-height: 1.45;">${escapeHtml(text)}</p>
|
||||
<button class="secondary-btn" type="button" id="profile-info-close">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const close = () => { root.innerHTML = ''; };
|
||||
root.querySelector('#profile-info-close')?.addEventListener('click', close);
|
||||
root.querySelector('#profile-info-modal')?.addEventListener('click', (event) => {
|
||||
if (event.target?.id === 'profile-info-modal') close();
|
||||
});
|
||||
function statusBadges(card){
|
||||
const role=card.accountRole==='primary'?'Основной аккаунт':card.accountRole==='non_voting'?'Голос не учитывать':'';
|
||||
const shine=card.shineStatus==='shining'?'Сияющий':'';
|
||||
return `<div class="row wrap-row">${role?`<span class="badge">${escapeHtml(role)}</span>`:''}${shine?'<span class="badge is-yes-shine">Сияющий</span>':''}${card.shineStatus==='not_interested'?'<span class="profile-shine-not-interested" title="Сияние неинтересно"><img src="/assets/shine-status-not-interested.svg" alt="Сияние неинтересно"></span>':''}</div>`;
|
||||
}
|
||||
|
||||
function officialInfoText() {
|
||||
return 'Можно создавать несколько альтернативных или анонимных каналов. '
|
||||
+ 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.';
|
||||
function statsHtml(card){
|
||||
const s=card.stats||{};
|
||||
const rows=[
|
||||
['friends','Друзья',s.friendsCount],['close_friends','Близкие друзья',s.closeFriendsCount],
|
||||
['primary_received','Подтвердили основной аккаунт',s.primaryReceivedCount],['primary_given','Подтверждённые аккаунты',s.primaryGivenCount],
|
||||
['shine_received','Считают сияющим',s.shineReceivedCount],['shine_given','Подтверждённые сияющие',s.shineGivenCount],
|
||||
['channels_following','Подписки на каналы',s.followingChannelsCount],['channels_owned','Каналы',s.ownedPublicChannelsCount],
|
||||
];
|
||||
return `<div class="profile-stats-grid">${rows.map(([kind,label,n])=>`<button type="button" class="card profile-stat-card" data-profile-list="${kind}"><b>${Number(n||0)}</b><span>${escapeHtml(label)}</span></button>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function shineInfoText() {
|
||||
return 'Сияющие — это те, от кого идёт внутреннее сияние на тонком плане.\n\n'
|
||||
+ 'Пять принципов сияющих:\n'
|
||||
+ '1) сияющие не обманывают;\n'
|
||||
+ '2) сияющие чувствуют, что человек — это не только физическое тело, а нечто большее;\n'
|
||||
+ '3) сияющие развиваются и в духовной, и в материальной плоскости;\n'
|
||||
+ '4) у сияющих есть близкие друзья, с которыми им по-настоящему хорошо;\n'
|
||||
+ '5) сияющие заботятся о мире: о людях, гармонии и общем благе.';
|
||||
}
|
||||
|
||||
function genderText(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'male') return 'Мужской';
|
||||
if (normalized === 'female') return 'Женский';
|
||||
return 'Не указан';
|
||||
}
|
||||
|
||||
function relationButtonLabel(kind, flags) {
|
||||
if (kind === 'contact') return flags.outContact ? 'Убрать из контактов' : 'Добавить в контакты';
|
||||
if (kind === 'friend') return flags.outFriend ? 'Убрать из друзей' : 'Добавить в друзья';
|
||||
if (kind === 'close_friend') return flags.outCloseFriend ? 'Убрать из близких друзей' : 'Добавить в близкие друзья';
|
||||
if (kind === 'official_account') return flags.outOfficialAccount ? 'Снять подтверждение' : 'Подтвердить аккаунт';
|
||||
if (kind === 'shine_confirmed') return flags.outShineConfirmed ? 'Снять подтверждение' : 'Подтвердить сияющего';
|
||||
return '';
|
||||
}
|
||||
|
||||
function relationNextState(kind, flags) {
|
||||
if (kind === 'contact') return !flags.outContact;
|
||||
if (kind === 'friend') return !flags.outFriend;
|
||||
if (kind === 'close_friend') return !flags.outCloseFriend;
|
||||
if (kind === 'official_account') return !flags.outOfficialAccount;
|
||||
if (kind === 'shine_confirmed') return !flags.outShineConfirmed;
|
||||
return false;
|
||||
}
|
||||
|
||||
function relationConfirmLabel(kind) {
|
||||
if (kind === 'contact') return 'контакт';
|
||||
if (kind === 'friend') return 'статус друга';
|
||||
if (kind === 'close_friend') return 'статус близкого друга';
|
||||
if (kind === 'official_account') return 'подтверждение официального аккаунта';
|
||||
if (kind === 'shine_confirmed') return 'подтверждение сияющего';
|
||||
return 'связь';
|
||||
}
|
||||
|
||||
function relationStateText(kind, flags) {
|
||||
if (kind === 'contact') {
|
||||
if (flags.outContact && flags.inContact) return 'Вы обменялись контактами.';
|
||||
if (flags.outContact) return 'Вы добавили этот профиль в контакты.';
|
||||
if (flags.inContact) return 'Этот профиль добавил вас в контакты.';
|
||||
return '';
|
||||
}
|
||||
if (kind === 'friend') {
|
||||
if (flags.outFriend && flags.inFriend) return 'Вы взаимно считаете друг друга друзьями.';
|
||||
if (flags.outFriend) return 'Вы считаете этого человека другом.';
|
||||
if (flags.inFriend) return 'Этот человек считает вас другом.';
|
||||
return '';
|
||||
}
|
||||
if (kind === 'close_friend') {
|
||||
if (flags.outCloseFriend && flags.inCloseFriend) return 'Вы взаимно близкие друзья.';
|
||||
if (flags.outCloseFriend) return 'Вы считаете этого человека близким другом.';
|
||||
if (flags.inCloseFriend) return 'Этот человек считает вас близким другом.';
|
||||
return '';
|
||||
}
|
||||
if (kind === 'official_account') {
|
||||
return flags.outOfficialAccount
|
||||
? 'Вы подтверждаете, что это действительно официальный аккаунт этого человека.'
|
||||
: '';
|
||||
}
|
||||
if (kind === 'shine_confirmed') {
|
||||
return flags.outShineConfirmed
|
||||
? 'Вы подтверждаете, что этот человек сияющий.'
|
||||
: '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderRelations(flags) {
|
||||
const rows = [
|
||||
{ kind: 'contact', text: relationStateText('contact', flags), button: relationButtonLabel('contact', flags) },
|
||||
{ kind: 'friend', text: relationStateText('friend', flags), button: relationButtonLabel('friend', flags) },
|
||||
{ kind: 'close_friend', text: relationStateText('close_friend', flags), button: relationButtonLabel('close_friend', flags) },
|
||||
{ kind: 'official_account', text: relationStateText('official_account', flags), button: relationButtonLabel('official_account', flags) },
|
||||
{ kind: 'shine_confirmed', text: relationStateText('shine_confirmed', flags), button: relationButtonLabel('shine_confirmed', flags) },
|
||||
];
|
||||
|
||||
return `
|
||||
<div class="card stack user-relations-list" data-profile-relations="true">
|
||||
${rows.map((row) => `
|
||||
<div class="user-rel-row ${row.text ? '' : 'is-empty'}">
|
||||
<span class="user-rel-text">${escapeHtml(row.text)}</span>
|
||||
<button class="ghost-btn user-rel-action" type="button" data-relation-action="${row.kind}">${escapeHtml(row.button)}</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderIdentity(card) {
|
||||
const lines = buildIdentityLines({
|
||||
login: card.login,
|
||||
firstName: card.firstName,
|
||||
lastName: card.lastName,
|
||||
});
|
||||
|
||||
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: 'xl',
|
||||
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) {
|
||||
const accountRole = String(card.accountRole || '').trim().toLowerCase();
|
||||
const shineStatus = String(card.shineStatus || '').trim().toLowerCase();
|
||||
const accountLabel = accountRole === 'primary' ? 'Основной аккаунт' : accountRole === 'non_voting' ? 'Голос не учитывать' : '';
|
||||
const shineLabel = shineStatus === 'shining' ? 'Сияющий' : '';
|
||||
return `
|
||||
<div class="row wrap-row">
|
||||
${accountLabel ? `<button class="badge profile-badge-trigger ${accountRole === 'primary' ? 'is-yes-official' : 'is-no'}" type="button" data-profile-info="official">${escapeHtml(accountLabel)}</button>` : ''}
|
||||
${shineLabel ? `<button class="badge profile-badge-trigger is-yes-shine" type="button" data-profile-info="shine">${escapeHtml(shineLabel)}</button>` : ''}
|
||||
${shineStatus === 'not_interested' ? `<span class="profile-shine-not-interested" title="Сияние неинтересно"><img src="/assets/shine-status-not-interested.svg" alt="Сияние неинтересно" /></span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderReadOnlyParams(card) {
|
||||
const rows = [
|
||||
{ label: 'Имя', value: card.firstName },
|
||||
{ label: 'Фамилия', value: card.lastName },
|
||||
{ label: 'Пол', value: genderText(card.gender) },
|
||||
{ label: 'Адрес', value: card.address },
|
||||
{ label: 'Web', value: card.web },
|
||||
{ label: 'Телефон', value: card.phone },
|
||||
{ label: 'О себе', value: card.about },
|
||||
{ label: 'Духовный путь', value: card.spiritualPath },
|
||||
];
|
||||
|
||||
return `
|
||||
<div class="card stack profile-param-list">
|
||||
${rows.map((row) => `
|
||||
<div class="card profile-param-item row">
|
||||
<div class="profile-param-value"><b>${row.label}</b>: ${escapeHtml(String(row.value || '').trim() || 'не заполнено')}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function render({ navigate, route }) {
|
||||
const requestedLogin = String(route.params.login || '').trim();
|
||||
const sessionLogin = String(state.session.login || '').trim();
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const status = document.createElement('div');
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Загрузка профиля...';
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Профиль пользователя',
|
||||
leftAction: { label: '←', onClick: () => navigateBack() },
|
||||
rightActions: [{ label: 'Показать\nсвязи', onClick: () => navigate(makeProfileLinksRoute(requestedLogin || '')) }],
|
||||
}),
|
||||
status,
|
||||
body,
|
||||
);
|
||||
const linksHeaderBtn = screen.querySelector('.header-actions .icon-btn');
|
||||
linksHeaderBtn?.classList.add('profile-links-header-btn');
|
||||
|
||||
let currentCard = null;
|
||||
let currentFlags = null;
|
||||
let isBusy = false;
|
||||
|
||||
function syncActionButtons() {
|
||||
const kinds = ['contact', 'friend', 'close_friend', 'official_account', 'shine_confirmed'];
|
||||
if (!currentFlags) return;
|
||||
const isSelf = currentCard && currentCard.login.toLowerCase() === sessionLogin.toLowerCase();
|
||||
kinds.forEach((kind) => {
|
||||
const btn = body.querySelector(`[data-relation-action="${kind}"]`);
|
||||
if (!btn) return;
|
||||
btn.textContent = relationButtonLabel(kind, currentFlags);
|
||||
btn.disabled = Boolean(isSelf);
|
||||
});
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (!requestedLogin) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = 'Не передан login пользователя.';
|
||||
return;
|
||||
}
|
||||
|
||||
isBusy = true;
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Загрузка профиля...';
|
||||
|
||||
try {
|
||||
const card = await loadUserProfileCard(requestedLogin);
|
||||
const flags = await loadRelationsForPair({
|
||||
currentLogin: sessionLogin,
|
||||
targetLogin: card.login,
|
||||
});
|
||||
|
||||
currentCard = card;
|
||||
currentFlags = flags;
|
||||
|
||||
body.innerHTML = `
|
||||
${renderReadOnlyBadges(card)}
|
||||
${renderRelations(flags)}
|
||||
${renderReadOnlyParams(card)}
|
||||
`;
|
||||
const identityCard = document.createElement('div');
|
||||
identityCard.className = 'card stack';
|
||||
identityCard.append(renderIdentity(card));
|
||||
body.prepend(identityCard);
|
||||
|
||||
syncActionButtons();
|
||||
if (String(route?.params?.section || '').toLowerCase() === 'links') {
|
||||
const rel = body.querySelector('[data-profile-relations="true"]');
|
||||
rel?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
status.className = 'status-line is-available';
|
||||
status.textContent = 'Профиль обновлён.';
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = `Ошибка загрузки профиля: ${error.message || 'unknown'}`;
|
||||
window.alert(`Не удалось загрузить профиль: ${error.message || 'unknown'}`);
|
||||
} finally {
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onRelationAction(kind) {
|
||||
if (isBusy || !currentCard || !currentFlags) return;
|
||||
if (!sessionLogin) {
|
||||
window.alert('Для изменения связей нужен активный вход.');
|
||||
return;
|
||||
}
|
||||
if (!state.session.storagePwdInMemory) {
|
||||
window.alert('Нет storagePwd в памяти сессии. Выполните вход заново.');
|
||||
return;
|
||||
}
|
||||
|
||||
const nextEnabled = relationNextState(kind, currentFlags);
|
||||
const confirmed = window.confirm(
|
||||
`Изменить ${relationConfirmLabel(kind)} с пользователем ${currentCard.login}?\n` +
|
||||
'Будет отправлен AddBlock CONNECTION.',
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
isBusy = true;
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Сохранение отношения в блокчейн...';
|
||||
|
||||
try {
|
||||
await authService.setUserRelation({
|
||||
login: sessionLogin,
|
||||
toLogin: currentCard.login,
|
||||
kind,
|
||||
enabled: nextEnabled,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
});
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = `Ошибка изменения связи: ${error.message || 'unknown'}`;
|
||||
window.alert(`Не удалось изменить связь: ${error.message || 'unknown'}`);
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
body.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
const infoBtn = target.closest('[data-profile-info]');
|
||||
const infoKind = String(infoBtn?.getAttribute('data-profile-info') || '');
|
||||
if (infoKind === 'official') {
|
||||
openProfileInfoModal({
|
||||
title: 'Основной аккаунт',
|
||||
text: officialInfoText(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (infoKind === 'shine') {
|
||||
openProfileInfoModal({
|
||||
title: 'Справка о сияющих',
|
||||
text: shineInfoText(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const actionBtn = target.closest('[data-relation-action]');
|
||||
const kind = String(actionBtn?.getAttribute('data-relation-action') || '');
|
||||
if (!kind) return;
|
||||
void onRelationAction(kind);
|
||||
});
|
||||
|
||||
refresh();
|
||||
return screen;
|
||||
export function render({navigate,route}){
|
||||
const requestedLogin=String(route?.params?.login||'').trim(); const selfLogin=String(state.session.login||'').trim();
|
||||
const screen=document.createElement('section'); screen.className='stack'; const body=document.createElement('div');body.className='stack';const status=document.createElement('div');status.className='status-line';status.textContent='Загрузка профиля...';
|
||||
screen.append(renderHeader({title:'Профиль пользователя',leftAction:{label:'←',onClick:()=>navigateBack()}}),status,body);
|
||||
let card=null;
|
||||
async function refresh(){
|
||||
card=await loadUserProfileCard(requestedLogin); const isSelf=card.login.toLowerCase()===selfLogin.toLowerCase();
|
||||
body.innerHTML=`${statusBadges(card)}<div class="card profile-about" style="white-space:pre-wrap">${escapeHtml(card.about||'')}</div>${statsHtml(card)}
|
||||
<div class="row wrap-row"><button class="secondary-btn" data-profile-detail="contacts">Контакты</button><button class="secondary-btn" data-profile-detail="spiritual">Духовный путь</button></div>
|
||||
${!isSelf?`<div class="row profile-bottom-actions"><button class="icon-btn" data-bottom="chat" title="Диалог">💬</button><button class="icon-btn" data-bottom="links" title="Связи">✦</button><button class="primary-btn" data-bottom="add">Добавить</button></div>`:''}`;
|
||||
const identity=document.createElement('div');identity.className='card row';identity.style.gap='12px';identity.style.alignItems='center';
|
||||
identity.append(renderUserAvatar({login:card.login,firstName:card.firstName,lastName:card.lastName,avatar:card.avatar,size:'xl',className:'profile-avatar'}));
|
||||
const txt=document.createElement('div');txt.innerHTML=`<div class="profile-identity-line">${escapeHtml(fullName(card))}</div><div class="profile-identity-login">${escapeHtml(card.login)}</div>`;identity.append(txt);body.prepend(identity);status.textContent='';
|
||||
}
|
||||
body.addEventListener('click',e=>{const el=e.target.closest('[data-profile-list],[data-profile-detail],[data-bottom]');if(!el||!card)return;
|
||||
const list=el.dataset.profileList;if(list){navigate(`SHiNE/${encodeURIComponent(card.login)}/list/${encodeURIComponent(list)}`);return;}
|
||||
const detail=el.dataset.profileDetail;if(detail==='contacts'){openTextModal('Контакты',[card.web?`Links: ${card.web}`:'',card.phone?`Телефон: ${card.phone}`:'',card.address?`Адрес: ${card.address}`:''].filter(Boolean).join('\n')||'Не заполнено');return;} if(detail==='spiritual'){openTextModal('Духовный путь',card.spiritualPath);return;}
|
||||
const bottom=el.dataset.bottom;if(bottom==='chat')navigate(`chat/${encodeURIComponent(card.login)}`);if(bottom==='links')navigate(makeProfileLinksRoute(card.login));if(bottom==='add')navigate(`SHiNE/${encodeURIComponent(card.login)}/manage`);
|
||||
});
|
||||
refresh().catch(e=>{status.className='status-line is-unavailable';status.textContent=`Ошибка: ${e.message||'unknown'}`;}); return screen;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { authService, state } from '../state.js';
|
||||
import { loadRelationsForPair, loadUserProfileCard } from '../services/user-connections.js';
|
||||
import { navigateBack } from '../router.js';
|
||||
|
||||
export const pageMeta = { id: 'user-relation-manage-view', title: 'Добавить' };
|
||||
|
||||
function effectiveSocial(f) { if (f.outCloseFriend) return 'close_friend'; if (f.outFriend) return 'friend'; if (f.outContact) return 'contact'; return 'none'; }
|
||||
|
||||
export function render({ route }) {
|
||||
const targetLogin = String(route?.params?.login || '').trim(); const selfLogin = String(state.session.login || '').trim();
|
||||
const screen = document.createElement('section'); screen.className = 'stack';
|
||||
const body = document.createElement('div'); body.className = 'stack'; const status = document.createElement('div'); status.className = 'status-line';
|
||||
screen.append(renderHeader({ title: 'Добавить', leftAction: { label: '←', onClick: () => navigateBack() } }), status, body);
|
||||
let flags, selfCard, targetCard;
|
||||
|
||||
async function setKind(kind, enabled) { await authService.setUserRelation({ login: selfLogin, toLogin: targetLogin, kind, enabled, storagePwd: state.session.storagePwdInMemory }); }
|
||||
async function changeSocial(next) {
|
||||
const current = effectiveSocial(flags); if (current === next) return;
|
||||
if (next === 'none') { if (flags.outCloseFriend) await setKind('close_friend', false); if (flags.outFriend) await setKind('friend', false); if (flags.outContact) await setKind('contact', false); }
|
||||
if (next === 'contact') { if (flags.outCloseFriend) await setKind('close_friend', false); if (flags.outFriend) await setKind('friend', false); if (!flags.outContact) await setKind('contact', true); }
|
||||
if (next === 'friend') { if (flags.outCloseFriend) await setKind('close_friend', false); if (!flags.outFriend) await setKind('friend', true); }
|
||||
if (next === 'close_friend' && !flags.outCloseFriend) await setKind('close_friend', true);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
status.textContent = 'Загрузка...'; body.innerHTML = '';
|
||||
[selfCard, targetCard, flags] = await Promise.all([loadUserProfileCard(selfLogin), loadUserProfileCard(targetLogin), loadRelationsForPair({ currentLogin: selfLogin, targetLogin })]);
|
||||
const social = effectiveSocial(flags);
|
||||
const canPrimary = selfCard.accountRole === 'primary' && targetCard.accountRole !== 'non_voting';
|
||||
const canShine = selfCard.accountRole === 'primary' && selfCard.shineStatus === 'shining' && targetCard.shineStatus !== 'not_interested';
|
||||
body.innerHTML = `
|
||||
<div class="card stack"><b>Связь с ${targetLogin}</b>
|
||||
<div class="row wrap-row" data-social>${['none','contact','friend','close_friend'].map(v => `<button class="secondary-btn ${social===v?'is-active':''}" data-social-kind="${v}">${({none:'Нет',contact:'Контакт',friend:'Друг',close_friend:'Близкий друг'})[v]}</button>`).join('')}</div>
|
||||
</div>
|
||||
${canPrimary ? `<div class="card row"><span>Основной аккаунт</span><button class="secondary-btn" data-confirm="official_account">${flags.outOfficialAccount ? 'Убрать подтверждение' : 'Подтвердить'}</button></div>` : ''}
|
||||
${canShine ? `<div class="card row"><span>Сияющий</span><button class="secondary-btn" data-confirm="shine_confirmed">${flags.outShineConfirmed ? 'Убрать подтверждение' : 'Подтвердить'}</button></div>` : ''}
|
||||
`;
|
||||
status.textContent = '';
|
||||
}
|
||||
|
||||
body.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('button'); if (!btn) return;
|
||||
const social = btn.dataset.socialKind; const confirm = btn.dataset.confirm;
|
||||
try { status.textContent = 'Сохранение...'; if (social) await changeSocial(social); else if (confirm) { const enabled = confirm === 'official_account' ? !flags.outOfficialAccount : !flags.outShineConfirmed; await setKind(confirm, enabled); await refresh(); } }
|
||||
catch (err) { status.className='status-line is-unavailable'; status.textContent=`Ошибка: ${err.message || 'unknown'}`; }
|
||||
});
|
||||
refresh().catch(e => { status.className='status-line is-unavailable'; status.textContent=`Ошибка: ${e.message || 'unknown'}`; });
|
||||
return screen;
|
||||
}
|
||||
@@ -107,6 +107,12 @@ export function parseRouteFromPath(pathname = '') {
|
||||
if (section === 'links') {
|
||||
return { pageId: 'network-view', params: { mode: 'keep-history', login: shineLogin } };
|
||||
}
|
||||
if (section === 'manage') {
|
||||
return { pageId: 'user-relation-manage-view', params: { login: shineLogin } };
|
||||
}
|
||||
if (section === 'list') {
|
||||
return { pageId: 'user-profile-list-view', params: { login: shineLogin, kind: decodePart(segments[baseOffset + 1] || '') } };
|
||||
}
|
||||
if (section === 'channels') {
|
||||
const sub = decodePart(segments[baseOffset + 1] || '').toLowerCase();
|
||||
if (sub === 'owned') {
|
||||
|
||||
@@ -2905,6 +2905,18 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async listUserProfileRelations(login, listType, limit = 100, offset = 0) {
|
||||
const response = await this.ws.request('ListUserProfileRelations', { login, listType, limit, offset });
|
||||
if (response.status !== 200) throw opError('ListUserProfileRelations', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async listUserProfileChannels(login, mode, limit = 100, offset = 0) {
|
||||
const response = await this.ws.request('ListUserProfileChannels', { login, mode, limit, offset });
|
||||
if (response.status !== 200) throw opError('ListUserProfileChannels', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async searchUsers(prefix, options = {}) {
|
||||
const payload = { prefix };
|
||||
if (typeof options?.isServer === 'boolean') {
|
||||
|
||||
@@ -262,6 +262,16 @@ export async function loadUserProfileCard(login) {
|
||||
// Compatibility aliases for older graph/card consumers; values come only from the new profile schema.
|
||||
official: String(snapshot?.accountRole || '').trim().toLowerCase() === 'primary',
|
||||
shine: String(snapshot?.shineStatus || '').trim().toLowerCase() === 'shining',
|
||||
stats: {
|
||||
ownedPublicChannelsCount: Number(user.ownedPublicChannelsCount || 0),
|
||||
followingChannelsCount: Number(user.followingChannelsCount || 0),
|
||||
friendsCount: Number(user.friendsCount || 0),
|
||||
closeFriendsCount: Number(user.closeFriendsCount || 0),
|
||||
primaryReceivedCount: Number(user.primaryConfirmationsReceivedCount || 0),
|
||||
primaryGivenCount: Number(user.primaryConfirmationsGivenCount || 0),
|
||||
shineReceivedCount: Number(user.shineConfirmationsReceivedCount || 0),
|
||||
shineGivenCount: Number(user.shineConfirmationsGivenCount || 0),
|
||||
},
|
||||
avatar: snapshot?.avatar?.txId
|
||||
? {
|
||||
ar: String(snapshot.avatar.txId).trim(),
|
||||
|
||||
Reference in New Issue
Block a user