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('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } 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 `
Официальный: ${card.official ? 'Yes' : 'No'} Сияющий: ${card.shine ? 'Yes' : 'No'}
`; } 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 `
${rows.map((row) => `
${escapeHtml(row.text)}
`).join('')}
${opinionItems.map((item) => `
${escapeHtml(item.text)}
`).join('')}
Добавьте одну из этих трёх формулировок.
${hasOpinion ? 'Мнение уже добавлено.' : 'Пока нет дополнительной связи.'}
`; } 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) => ``) .join(''); const removeHtml = activeKind ? `` : ''; root.innerHTML = ` `; 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 `
${rows.map((row) => `
${row.label}: ${escapeHtml(String(row.value || '').trim() || 'не заполнено')}
`).join('')}
`; } 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; }