import { renderHeader } from '../components/header.js'; import { SHINE_CONNECTIONS_LOGO_SRC } from '../components/shine-logo.js'; import { renderUserAvatar } from '../components/avatar-image.js'; import { authService, state } from '../state.js'; import { loadRelationsForPair, loadUserProfileCard } from '../services/user-connections.js'; import { makeProfileLinksRoute } from '../services/shine-routes.js'; import { navigateBack } from '../router.js'; export const pageMeta = { id: 'user', title: 'Профиль' }; function escapeHtml(text) { return String(text || '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function effectiveSocial(flags = {}) { if (flags.outCloseFriend) return 'close_friend'; if (flags.outFriend) return 'friend'; if (flags.outContact) return 'contact'; return 'none'; } function metricHtml({ kind, label, value, glow = false, positionClass = '' }) { const numericValue = Number(value || 0); return ` `; } function openProfileSheet(title, html) { const root = document.getElementById('modal-root'); if (!root) return; root.innerHTML = `
`; const close = () => { if (root.querySelector('#user-profile-sheet-backdrop')) root.innerHTML = ''; }; root.querySelector('#user-profile-sheet-backdrop')?.addEventListener('click', (event) => { if (event.target?.id === 'user-profile-sheet-backdrop') close(); }); } function aboutSheetHtml(card) { return `
${escapeHtml(card?.about || 'Не заполнено')}
`; } function contactsSheetHtml(card) { const rows = [ ['Ссылки', card?.web], ['Телефон', card?.phone], ['Адрес', card?.address], ].filter(([, value]) => String(value || '').trim()); if (!rows.length) { return '
Не заполнено
'; } return rows.map(([label, value]) => `
${escapeHtml(label)} ${escapeHtml(value)}
`).join(''); } function addIconHtml() { return ` `; } function relationMenuHtml(flags = {}) { const current = effectiveSocial(flags); const rows = [ ['friend', 'Друг'], ['close_friend', 'Близкий друг'], ['contact', 'Контакт'], ]; return rows.map(([kind, label]) => ` `).join(''); } export function render({ navigate, route, chrome }) { const requestedLogin = String(route?.params?.login || '').trim(); const selfLogin = String(state.session.login || '').trim(); const screen = document.createElement('section'); screen.className = 'stack user-profile-screen'; const header = renderHeader({ title: requestedLogin || 'Профиль', leftAction: { label: '←', onClick: () => navigateBack() }, }); header.classList.add('user-profile-header'); chrome?.setTopbar(header); const status = document.createElement('div'); status.className = 'status-line user-profile-status'; status.textContent = 'Загрузка профиля...'; const body = document.createElement('div'); body.className = 'user-profile-body'; screen.append(status, body); let card = null; let relationFlags = null; let relationLoadPromise = null; let addMenu = null; let addActionButton = null; function updateRelationUi() { if (!addMenu) return; addMenu.innerHTML = relationMenuHtml(relationFlags || {}); const social = effectiveSocial(relationFlags || {}); addActionButton?.classList.toggle('is-active', social !== 'none'); addActionButton?.setAttribute('aria-label', social === 'none' ? 'Добавить' : 'Изменить связь'); } async function ensureRelationFlags() { if (relationFlags) return relationFlags; if (relationLoadPromise) return relationLoadPromise; if (!selfLogin || !card?.login) return {}; relationLoadPromise = loadRelationsForPair({ currentLogin: selfLogin, targetLogin: card.login }) .then((flags) => { relationFlags = flags || {}; updateRelationUi(); return relationFlags; }) .finally(() => { relationLoadPromise = null; }); return relationLoadPromise; } async function setRelationKind(kind, enabled) { await authService.setUserRelation({ login: selfLogin, toLogin: card.login, kind, enabled, storagePwd: state.session.storagePwdInMemory, }); } async function changeSocial(next) { const flags = await ensureRelationFlags(); const current = effectiveSocial(flags); if (current === next) return; if (next === 'contact') { if (flags.outCloseFriend) await setRelationKind('close_friend', false); if (flags.outFriend) await setRelationKind('friend', false); if (!flags.outContact) await setRelationKind('contact', true); } if (next === 'friend') { if (flags.outCloseFriend) await setRelationKind('close_friend', false); if (!flags.outFriend) await setRelationKind('friend', true); } if (next === 'close_friend' && !flags.outCloseFriend) { await setRelationKind('close_friend', true); } relationFlags = await loadRelationsForPair({ currentLogin: selfLogin, targetLogin: card.login }); updateRelationUi(); } function renderProfile() { if (!card) return; const isSelf = card.login.toLowerCase() === selfLogin.toLowerCase(); const stats = card.stats || {}; const official = card.accountRole === 'primary'; const shining = card.shineStatus === 'shining'; const fullName = [card.firstName, card.lastName] .map((value) => String(value || '').trim()) .filter(Boolean) .join(' '); const title = header.querySelector('.page-title'); if (title) title.textContent = card.login; body.innerHTML = ` ${fullName ? `
${escapeHtml(fullName)}
` : ''}
${metricHtml({ kind: 'primary_received', label: 'Официальный', value: stats.primaryReceivedCount, glow: official, positionClass: 'is-top-left' })} ${metricHtml({ kind: 'shine_received', label: 'Сияющий', value: stats.shineReceivedCount, glow: shining, positionClass: 'is-top-right' })}
${metricHtml({ kind: 'friends', label: 'Друзья', value: stats.friendsCount, positionClass: 'is-bottom-left' })} ${metricHtml({ kind: 'close_friends', label: 'Близкие друзья', value: stats.closeFriendsCount, positionClass: 'is-bottom-right' })}
${metricHtml({ kind: 'channels_following', label: 'Подписки', value: stats.followingChannelsCount, positionClass: 'is-channel-metric' })} ${metricHtml({ kind: 'channels_owned', label: 'Каналы', value: stats.ownedPublicChannelsCount, positionClass: 'is-channel-metric' })}
${!isSelf ? `
` : ''} `; const avatarSlot = body.querySelector('.user-profile-avatar-slot'); avatarSlot?.append(renderUserAvatar({ login: card.login, firstName: card.firstName, lastName: card.lastName, avatar: card.avatar, size: 'xl', className: 'user-profile-hero-avatar', glow: shining, })); addMenu = body.querySelector('.user-profile-add-menu'); addActionButton = body.querySelector('[data-profile-action="add"]'); updateRelationUi(); status.textContent = ''; if (!isSelf && selfLogin) { void ensureRelationFlags().catch(() => {}); } } body.addEventListener('click', async (event) => { if (!card) return; const listButton = event.target.closest('[data-profile-list]'); if (listButton) { const kind = listButton.dataset.profileList; if (kind) navigate(`SHiNE/${encodeURIComponent(card.login)}/list/${encodeURIComponent(kind)}`); return; } const detailButton = event.target.closest('[data-profile-detail]'); if (detailButton?.dataset.profileDetail === 'about') { openProfileSheet('О себе', aboutSheetHtml(card)); return; } if (detailButton?.dataset.profileDetail === 'contacts') { openProfileSheet('Контакты', contactsSheetHtml(card)); return; } const relationButton = event.target.closest('[data-relation-kind]'); if (relationButton) { if (!selfLogin) { status.className = 'status-line user-profile-status is-unavailable'; status.textContent = 'Для добавления пользователя необходимо войти.'; return; } const next = relationButton.dataset.relationKind; try { addMenu?.classList.add('is-busy'); await changeSocial(next); if (addMenu) addMenu.hidden = true; addActionButton?.setAttribute('aria-expanded', 'false'); status.className = 'status-line user-profile-status'; status.textContent = ''; } catch (error) { status.className = 'status-line user-profile-status is-unavailable'; status.textContent = `Ошибка: ${error?.message || 'Не удалось изменить связь'}`; } finally { addMenu?.classList.remove('is-busy'); } return; } const actionButton = event.target.closest('[data-profile-action]'); const action = actionButton?.dataset.profileAction; if (action === 'add') { if (!addMenu) return; navigate(`SHiNE/${encodeURIComponent(card.login)}/manage`); return; } if (action === 'links') { navigate(makeProfileLinksRoute(card.login)); return; } if (action === 'chat') { navigate(`chat/${encodeURIComponent(card.login)}`); } }); const handleOutsidePointer = (event) => { if (!addMenu || addMenu.hidden) return; if (event.target instanceof Node && body.contains(event.target)) { const insideActions = event.target.closest?.('.user-profile-actions-wrap'); if (insideActions) return; } addMenu.hidden = true; addActionButton?.setAttribute('aria-expanded', 'false'); }; document.addEventListener('pointerdown', handleOutsidePointer); async function refresh() { card = await loadUserProfileCard(requestedLogin); renderProfile(); } refresh().catch((error) => { status.className = 'status-line user-profile-status is-unavailable'; status.textContent = `Ошибка: ${error?.message || 'unknown'}`; }); screen.cleanup = () => { document.removeEventListener('pointerdown', handleOutsidePointer); const root = document.getElementById('modal-root'); if (root?.querySelector('#user-profile-sheet-backdrop')) root.innerHTML = ''; }; return screen; }