SHiNE-server/shine-UI/js/pages/user-profile-view.js

441 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { renderHeader } from '../components/header.js';
import { authService, state } from '../state.js';
import {
buildIdentityLines,
loadRelationsForPair,
loadUserProfileCard,
} from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { navigateBack } from '../router.js';
export const pageMeta = { id: 'user', title: 'Чужой профиль' };
function escapeHtml(text) {
return String(text || '')
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
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 ? 'Убрать из близких друзей' : 'Добавить в близкие друзья';
return flags.outFollow ? 'Отписаться' : 'Подписаться';
}
function relationNextState(kind, flags) {
if (kind === 'contact') return !flags.outContact;
if (kind === 'friend') return !flags.outFriend;
return !flags.outFollow;
}
function relationConfirmLabel(kind) {
if (kind === 'contact') return 'контакт';
if (kind === 'friend') 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 (flags.outFollow && flags.inFollow) return 'Вы взаимно подписаны.';
if (flags.outFollow) return 'Вы подписаны на этот профиль.';
if (flags.inFollow) return 'Этот профиль подписан на вас.';
return '';
}
function opinionItemsFromFlags(flags) {
const items = [];
if (flags.outShineSeen) {
items.push({
kind: 'shine_seen',
text: 'вы утверждаете, что очень мало знаете этого человека, но вы видели его сияющим, и всё, что вы о нём знаете, подтверждает это',
label: 'видел сияющим',
});
}
if (flags.outShineConfirmed) {
items.push({
kind: 'shine_confirmed',
text: 'вы утверждаете, что достаточно хорошо знаете этого человека и точно уверены, что этот человек сияющий',
label: 'точно сияющий',
});
}
if (flags.outKnownPerson) {
items.push({
kind: 'known_person',
text: 'вы утверждаете, что просто знаете этого человека',
label: 'просто знаю',
});
}
return items;
}
function resolveActiveOpinionKind(flags) {
if (flags.outShineSeen) return 'shine_seen';
if (flags.outShineConfirmed) return 'shine_confirmed';
if (flags.outKnownPerson) return 'known_person';
return '';
}
function opinionLabelByKind(kind) {
if (kind === 'shine_seen') return 'мало знаком, но видел сияющим';
if (kind === 'shine_confirmed') return 'точно уверен, что сияющий';
if (kind === 'known_person') return 'просто знаю человека';
return kind;
}
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: '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) {
return `
<div class="row wrap-row">
<span class="badge ${card.official ? 'is-yes-official' : 'is-no'}">Официальный: ${card.official ? 'Yes' : 'No'}</span>
<span class="badge ${card.shine ? 'is-yes-shine' : 'is-no'}">Сияющий: ${card.shine ? 'Yes' : 'No'}</span>
</div>
`;
}
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: 'follow', text: relationStateText('follow', flags), button: relationButtonLabel('follow', flags) },
];
const opinionItems = opinionItemsFromFlags(flags);
const hasOpinion = opinionItems.length > 0;
return `
<div class="card stack user-relations-list">
${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 class="user-rel-opinions-wrap ${hasOpinion ? '' : 'is-empty'}">
<div class="user-rel-opinions-list">
${opinionItems.map((item) => `
<div class="user-rel-opinion-item">${escapeHtml(item.text)}</div>
`).join('')}
</div>
<div class="user-rel-opinions-hint">Добавьте одну из этих трёх формулировок.</div>
</div>
<div class="user-rel-row">
<span class="user-rel-text">${hasOpinion ? 'Мнение уже добавлено.' : 'Пока нет дополнительной связи.'}</span>
<button class="ghost-btn user-rel-action user-rel-opinion-btn" type="button" data-relation-action="opinion-menu">${hasOpinion ? 'Изменить связи' : 'Добавить связь'}</button>
</div>
</div>
`;
}
function openOpinionMenuModal({ flags, onApply }) {
const root = document.getElementById('modal-root');
if (!root) return;
const activeKind = resolveActiveOpinionKind(flags);
const items = [
{ kind: 'known_person', title: 'просто знаю человека' },
{ kind: 'shine_confirmed', title: 'точно уверен, что сияющий' },
{ kind: 'shine_seen', title: 'мало знаком, но видел сияющим' },
];
const rowsHtml = items
.filter((item) => item.kind !== activeKind)
.map((item) => `<button class="secondary-btn user-opinion-modal-btn is-add" type="button" data-opinion-kind="${item.kind}" data-opinion-mode="set">Добавить: ${item.title}</button>`)
.join('');
const removeHtml = activeKind
? `<button class="secondary-btn user-opinion-modal-btn is-remove" type="button" data-opinion-kind="${activeKind}" data-opinion-mode="remove">Убрать мнение</button>`
: '';
root.innerHTML = `
<div class="modal" id="user-opinion-modal">
<div class="modal-card stack">
<h3 class="modal-title">${activeKind ? 'Изменить связи' : 'Добавить связь'}</h3>
<div class="stack">${rowsHtml}${removeHtml}</div>
<button class="secondary-btn" type="button" id="user-opinion-modal-close">Закрыть</button>
</div>
</div>
`;
const close = () => { root.innerHTML = ''; };
root.querySelector('#user-opinion-modal-close')?.addEventListener('click', close);
root.querySelector('#user-opinion-modal')?.addEventListener('click', (event) => {
if (event.target?.id === 'user-opinion-modal') close();
});
root.querySelectorAll('[data-opinion-mode]').forEach((btn) => {
btn.addEventListener('click', async () => {
const nextKind = String(btn.getAttribute('data-opinion-kind') || '').trim();
const mode = String(btn.getAttribute('data-opinion-mode') || '').trim();
close();
if (!nextKind) return;
await onApply({ mode, nextKind, activeKind });
});
});
}
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 },
];
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: 'Обновить', onClick: () => refresh() }],
}),
status,
body,
);
let currentCard = null;
let currentFlags = null;
let isBusy = false;
function syncActionButtons() {
const followBtn = body.querySelector('[data-relation-action="follow"]');
const friendBtn = body.querySelector('[data-relation-action="friend"]');
const contactBtn = body.querySelector('[data-relation-action="contact"]');
const opinionBtn = body.querySelector('[data-relation-action="opinion-menu"]');
if (!followBtn || !friendBtn || !contactBtn || !opinionBtn || !currentFlags) return;
const isSelf = currentCard && currentCard.login.toLowerCase() === sessionLogin.toLowerCase();
contactBtn.textContent = relationButtonLabel('contact', currentFlags);
friendBtn.textContent = relationButtonLabel('friend', currentFlags);
followBtn.textContent = relationButtonLabel('follow', currentFlags);
contactBtn.disabled = Boolean(isSelf);
friendBtn.disabled = Boolean(isSelf);
followBtn.disabled = Boolean(isSelf);
opinionBtn.textContent = opinionItemsFromFlags(currentFlags).length ? 'Изменить связи' : 'Добавить связь';
opinionBtn.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();
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;
}
if (kind === 'opinion-menu') {
openOpinionMenuModal({
flags: currentFlags,
onApply: onOpinionApply,
});
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;
}
}
async function onOpinionApply({ mode, nextKind, activeKind }) {
if (isBusy || !currentCard || !currentFlags) return;
if (!sessionLogin) {
window.alert('Для изменения связей нужен активный вход.');
return;
}
if (!state.session.storagePwdInMemory) {
window.alert('Нет storagePwd в памяти сессии. Выполните вход заново.');
return;
}
const confirmed = window.confirm(`Изменить мнение о пользователе ${currentCard.login}?`);
if (!confirmed) return;
isBusy = true;
status.className = 'status-line';
status.textContent = 'Сохранение отношения в блокчейн...';
try {
if (activeKind) {
await authService.setUserRelation({
login: sessionLogin,
toLogin: currentCard.login,
kind: activeKind,
enabled: false,
storagePwd: state.session.storagePwdInMemory,
});
}
if (mode === 'set') {
await authService.setUserRelation({
login: sessionLogin,
toLogin: currentCard.login,
kind: nextKind,
enabled: true,
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 actionBtn = target.closest('[data-relation-action]');
const kind = String(actionBtn?.getAttribute('data-relation-action') || '');
if (!kind) return;
void onRelationAction(kind);
});
refresh();
return screen;
}