From 52a9f1ac1dbb4db377e3d31c66dd9d4262a6bbdd1980f6cc9916d51e03581a7d Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 1 Sep 2026 18:45:32 +0400 Subject: [PATCH 1/4] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20UI=20=D1=81=D0=B5=D1=82=D0=B8=20=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=84=D0=B8=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 2 +- shine-UI/js/pages/chat-view.js | 10 +- shine-UI/js/pages/messages-list.js | 84 +---- shine-UI/js/pages/network-view.js | 10 +- shine-UI/js/pages/network/adapter.js | 12 +- shine-UI/js/pages/network/force-graph.js | 3 +- shine-UI/js/pages/network/node-menu.js | 4 +- shine-UI/js/pages/profile-view.js | 385 +++++++------------- shine-UI/js/pages/user-profile-list-view.js | 11 +- shine-UI/js/services/user-display.js | 6 + shine-UI/js/services/user-profile-params.js | 2 +- shine-UI/styles/components.css | 5 + 12 files changed, 208 insertions(+), 326 deletions(-) create mode 100644 shine-UI/js/services/user-display.js diff --git a/VERSION.properties b/VERSION.properties index 4b959120..170f393a 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.11.0 +client.version=1.11.1 server.version=1.9.0 diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index 5ca75ded..71dd9512 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -30,6 +30,7 @@ import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speec import { showToast } from '../services/channels-ux.js'; import { buildDmReplyTechBlock, parseDmTechBlocks, sanitizeUserDmTextForSend } from '../services/dm-tech-blocks.js'; import { loadProfileSnapshot } from '../services/user-profile-params.js'; +import { userDisplayName } from '../services/user-display.js'; import { makeProfileLinksRoute, makeProfileRoute } from '../services/shine-routes.js'; export const pageMeta = { id: 'chat-view', title: 'Чат' }; @@ -127,7 +128,7 @@ function createChatHeaderParts(login, navigate) { loginEl.setAttribute('role', 'heading'); loginEl.setAttribute('aria-level', '1'); loginEl.setAttribute('aria-label', `Чат с ${cleanLogin}`); - loginEl.textContent = cleanLogin; + loginEl.innerHTML = `${cleanLogin}${cleanLogin}`; void loadProfileSnapshot(cleanLogin) .then((snapshot) => { @@ -147,6 +148,13 @@ function createChatHeaderParts(login, navigate) { title: cleanLogin, }); avatarSlot.replaceChildren(upgradedAvatar); + if (loginEl.isConnected) { + const display = userDisplayName({ login: cleanLogin, firstName: snapshot?.firstName, lastName: snapshot?.lastName }); + const nameNode = loginEl.querySelector('.chat-header-display-name'); + const loginNode = loginEl.querySelector('.chat-header-user-login'); + if (nameNode) nameNode.textContent = display; + if (loginNode) loginNode.textContent = cleanLogin; + } }) .catch(() => {}); diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index 63c36937..6c4f8b90 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -11,9 +11,9 @@ import { renderUserAvatar } from '../components/avatar-image.js'; import { createOverflowDots } from '../components/overflow-dots.js'; import { createDropdownMenu } from '../components/dropdown-menu.js'; import { createShineConnectionsLogo } from '../components/shine-logo.js'; -import { loadProfileSnapshot } from '../services/user-profile-params.js'; import { parseDmTechBlocks } from '../services/dm-tech-blocks.js'; import { formatRelativeTime } from '../services/channels-ux.js'; +import { userDisplayName } from '../services/user-display.js'; export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' }; const PREVIEW_MAX_LEN = 200; @@ -24,69 +24,9 @@ const SVG_CHEVRON = ` `; const DM_BLOB_PREVIEW_CACHE = new Map(); const DM_BLOB_PREVIEW_PENDING = new Map(); -const dmAvatarSnapshotCache = new Map(); -const dmAvatarPendingByLogin = new Map(); - -const RELATION_ORDER = new Map([ - ['close_friend', 0], - ['contact', 1], - ['none', 2], -]); - -async function loadDmAvatarSnapshot(login) { - const cleanLogin = String(login || '').trim(); - if (!cleanLogin) return null; - const key = cleanLogin.toLowerCase(); - if (dmAvatarSnapshotCache.has(key)) return dmAvatarSnapshotCache.get(key); - if (dmAvatarPendingByLogin.has(key)) return dmAvatarPendingByLogin.get(key); - const pending = loadProfileSnapshot(cleanLogin) - .then((snapshot) => { - dmAvatarSnapshotCache.set(key, snapshot || null); - dmAvatarPendingByLogin.delete(key); - return snapshot || null; - }) - .catch(() => { - dmAvatarSnapshotCache.set(key, null); - dmAvatarPendingByLogin.delete(key); - return null; - }); - dmAvatarPendingByLogin.set(key, pending); - return pending; -} - -function createDmAvatar(login, { className = '' } = {}) { - const cleanLogin = String(login || '').trim(); - const title = cleanLogin ? `Профиль ${cleanLogin}` : ''; - const avatarEl = renderUserAvatar({ - login: cleanLogin || 'unknown', - size: 'lg', - title, - className, - }); - if (!cleanLogin) return avatarEl; - void loadDmAvatarSnapshot(cleanLogin).then((snapshot) => { - if (!avatarEl.isConnected) return; - const upgraded = renderUserAvatar({ - login: cleanLogin, - avatar: snapshot?.avatar?.txId - ? { - ar: String(snapshot.avatar.txId || '').trim(), - sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(), - } - : null, - size: 'lg', - title, - className, - }); - upgraded.classList.add('avatar'); - avatarEl.replaceWith(upgraded); - }); - return avatarEl; -} - function normalizeRelationFlag(value) { const clean = String(value || '').trim().toLowerCase(); - if (clean === 'close_friend' || clean === 'contact') return clean; + if (clean === 'close_friend' || clean === 'friend' || clean === 'contact') return clean; return 'none'; } @@ -98,6 +38,8 @@ function relationLabel(flag) { switch (normalizeRelationFlag(flag)) { case 'close_friend': return 'близкий друг'; + case 'friend': + return 'друг'; case 'contact': return 'контакт'; default: @@ -225,6 +167,7 @@ export function render({ navigate, chrome }) { const filterLabels = { all: 'Чаты', close_friend: 'Близкие друзья', + friend: 'Друзья', contact: 'Контакты', none: 'Новые', }; @@ -237,6 +180,7 @@ export function render({ navigate, chrome }) { items: [ { label: 'Все чаты', action: () => { currentChatFilter = 'all'; filterTitle.textContent = filterLabels.all; reloadForFilter(); } }, { label: 'Близкие друзья', action: () => { currentChatFilter = 'close_friend'; filterTitle.textContent = filterLabels.close_friend; reloadForFilter(); } }, + { label: 'Друзья', action: () => { currentChatFilter = 'friend'; filterTitle.textContent = filterLabels.friend; reloadForFilter(); } }, { label: 'Контакты', action: () => { currentChatFilter = 'contact'; filterTitle.textContent = filterLabels.contact; reloadForFilter(); } }, { label: 'Новые', action: () => { currentChatFilter = 'none'; filterTitle.textContent = filterLabels.none; reloadForFilter(); } }, ], @@ -338,7 +282,14 @@ function renderRow(item) { const relationBadge = relationFlag === 'none' ? 'не в контактах' : relationLabel(relationFlag); - const avatarEl = createDmAvatar(item.peerLogin); + const avatarEl = renderUserAvatar({ + login: item.peerLogin, + firstName: item.firstName, + lastName: item.lastName, + avatar: item.avatarAr ? { ar: String(item.avatarAr).trim() } : null, + size: 'lg', + title: `Профиль ${item.peerLogin}`, + }); avatarEl.classList.add('avatar'); const avatarWrap = document.createElement('div'); avatarWrap.className = 'dm-av dm-av--default'; @@ -362,7 +313,7 @@ function renderRow(item) { const titleEl = row.querySelector('.dm-row-title'); const previewEl = row.querySelector('.dm-row-last-message'); const timeEl = row.querySelector('.dm-row-time'); - if (titleEl) titleEl.textContent = String(item.peerLogin || ''); + if (titleEl) titleEl.textContent = userDisplayName(item); if (previewEl) previewEl.textContent = 'Загрузка…'; if (timeEl) timeEl.textContent = formatChatRowTime(item.lastMessageTimeMs); row.prepend(avatarWrap); @@ -399,6 +350,11 @@ function renderRow(item) { const next = { id: peerLogin, peerLogin, + firstName: String(dialog?.firstName || '').trim(), + lastName: String(dialog?.lastName || '').trim(), + avatarAr: String(dialog?.avatarAr || '').trim(), + accountRole: String(dialog?.accountRole || '').trim(), + shineStatus: String(dialog?.shineStatus || '').trim(), relationFlag, lastMessageBlobB64: String(dialog?.lastMessageBlobB64 || ''), lastMessageTimeMs: Number(dialog?.lastMessageTimeMs || 0), diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js index 3acfc0c1..56c40963 100644 --- a/shine-UI/js/pages/network-view.js +++ b/shine-UI/js/pages/network-view.js @@ -5,6 +5,7 @@ import { makeProfileLinksRoute } from '../services/shine-routes.js'; import { createForceGraph } from './network/force-graph.js'; import { engineModelFromGraphModel } from './network/adapter.js'; import { openNodeMenu } from './network/node-menu.js'; +import { userDisplayName } from '../services/user-display.js'; export const pageMeta = { id: 'network-view', title: 'Связи' }; @@ -99,6 +100,12 @@ function getMarkByLogin(allUsers) { if (!login) return; map.set(normKey(login), { login, + firstName: String(row?.firstName || '').trim(), + lastName: String(row?.lastName || '').trim(), + displayName: userDisplayName({ login, firstName: row?.firstName, lastName: row?.lastName }), + relationType: String(row?.relationType || '').trim().toLowerCase(), + primaryConfirmed: Boolean(row?.primaryConfirmed), + shineConfirmed: Boolean(row?.shineConfirmed), official: Boolean(row?.official), shine: Boolean(row?.shine), officialLabel: String(row?.officialLabel || (row?.official ? 'официальный' : 'неофициальный')), @@ -236,7 +243,7 @@ export function render({ navigate, route, chrome } = {}) { // Фильтры слоёв (Фаза 3). Фокус всегда виден; предикат применяется к периферийным узлам. const FILTERS = { all: { label: 'Все', pred: () => true }, - friends: { label: 'Друзья', pred: (n) => n.relationType === 'friend' }, + friends: { label: 'Друзья', pred: (n) => n.relationType === 'friend' || n.relationType === 'close_friend' }, shining: { label: 'Сияющие', pred: (n) => Boolean(n.shining) }, }; const FILTER_ORDER = ['all', 'friends', 'shining']; @@ -412,6 +419,7 @@ export function render({ navigate, route, chrome } = {}) { const login = normalizeLogin(node.login); openNodeMenu({ login, + displayName: String(node.name || '').trim(), relationType: node.relationType, point, actions: [ diff --git a/shine-UI/js/pages/network/adapter.js b/shine-UI/js/pages/network/adapter.js index 2ea11e28..81f33cc0 100644 --- a/shine-UI/js/pages/network/adapter.js +++ b/shine-UI/js/pages/network/adapter.js @@ -10,7 +10,10 @@ function normLogin(value) { const FAMILY_ROLES = ['parent', 'child', 'spouse', 'sibling']; -function relationTypeFromRole(role) { +function relationTypeFromRelation(relation) { + const explicit = String(relation?.mark?.relationType || '').trim().toLowerCase(); + if (explicit === 'close_friend' || explicit === 'friend' || explicit === 'contact') return explicit; + const role = relation?.role; if (FAMILY_ROLES.includes(role)) return 'family'; if (role === 'friend') return 'friend'; return 'contact'; @@ -22,6 +25,7 @@ function relationTypeFromRole(role) { // - взаимные дружеские связи ближе односторонних. function deriveStrength(relation) { if (relation.isRelative) return 0.9; + if (relation?.mark?.relationType === 'close_friend') return 0.9; if (relation.role === 'contact') return 0.4; if (relation.forward && relation.backward) return 0.8; return 0.55; @@ -38,7 +42,7 @@ export function engineModelFromGraphModel(graphModel) { const focusNode = { id: focusLogin, login: focusLogin, - name: '', + name: centerMark?.displayName || centerMark?.login || focusLogin, avatar: centerMark?.avatar || null, relationType: 'self', strength: 1, @@ -58,9 +62,9 @@ export function engineModelFromGraphModel(graphModel) { return { id: login, login, - name: '', + name: r?.mark?.displayName || login, avatar: r?.mark?.avatar || null, - relationType: relationTypeFromRole(r?.role), + relationType: relationTypeFromRelation(r), strength: deriveStrength(r || {}), shining: Boolean(r?.mark?.shine), tier: 1, diff --git a/shine-UI/js/pages/network/force-graph.js b/shine-UI/js/pages/network/force-graph.js index 66728f09..0f8adaf3 100644 --- a/shine-UI/js/pages/network/force-graph.js +++ b/shine-UI/js/pages/network/force-graph.js @@ -86,6 +86,7 @@ const DOUBLE_TAP_MS = 320; // окно двойного тапа по фо const RELATION_COLORS = { family: 'rgba(255, 159, 94, 0.92)', friend: 'rgba(120, 179, 255, 0.9)', + close_friend: 'rgba(120, 179, 255, 0.98)', business: 'rgba(190, 150, 255, 0.9)', contact: 'rgba(170, 190, 220, 0.7)', }; @@ -593,7 +594,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL function updateA11y() { const focus = nodes.find((n) => n.isFocus); const tier1 = nodes.filter((n) => n.tier === 1 && !n.isFocus); - const rel = { family: 'семья', friend: 'друг', business: 'бизнес', contact: 'контакт' }; + const rel = { family: 'семья', friend: 'друг', close_friend: 'близкий друг', business: 'бизнес', contact: 'контакт' }; const items = tier1.map((n) => `
  • ${escapeHtml(n.name || n.login || String(n.id))}${n.shining ? ' — сияющий' : ''} (${rel[n.relationType] || 'связь'})
  • `).join(''); a11y.innerHTML = `

    Центр: ${escapeHtml(focus ? (focus.name || focus.login || '') : '')}. Связей 1-го уровня: ${tier1.length}.

    `; } diff --git a/shine-UI/js/pages/network/node-menu.js b/shine-UI/js/pages/network/node-menu.js index ed38d6cd..1f323a0c 100644 --- a/shine-UI/js/pages/network/node-menu.js +++ b/shine-UI/js/pages/network/node-menu.js @@ -31,7 +31,7 @@ export function relationLabelRu(relationType) { * @param {{x:number,y:number,rect?:DOMRect}} opts.point - экранная точка/rect узла * @param {Array<{label:string, onClick:Function, disabled?:boolean}>} opts.actions - пункты меню */ -export function openNodeMenu({ login, relationType, point, actions = [] } = {}) { +export function openNodeMenu({ login, displayName = '', relationType, point, actions = [] } = {}) { const root = document.getElementById('modal-root'); if (!(root instanceof HTMLElement)) return; @@ -43,7 +43,7 @@ export function openNodeMenu({ login, relationType, point, actions = [] } = {})
    `; + const menuButton = topbar.querySelector('.profile-head-menu-btn'); + menuButton?.append(createOverflowDots()); const profileMenu = createDropdownMenu({ - anchorEl: profileMenuButton, + anchorEl: menuButton, className: 'profile-head-menu', minWidth: 230, items: [ @@ -123,197 +93,114 @@ export function render({ navigate, chrome }) { { label: 'Настройки', iconSrc: '/assets/profile-icon-settings.svg', action: () => navigate('settings-view') }, ], }); - chrome?.setTopbar(topbar); - const card = document.createElement('div'); - card.className = 'card stack profile-main-card'; + const status = document.createElement('div'); + status.className = 'status-line'; + status.textContent = 'Загрузка профиля...'; + const body = document.createElement('div'); + body.className = 'stack'; + screen.append(status, body); - const topRow = document.createElement('div'); - topRow.className = 'row'; - topRow.innerHTML = ` -
    -
    -
    - -
    -
    - `; + let current = null; - const badgesRow = document.createElement('div'); - badgesRow.className = 'row'; - badgesRow.innerHTML = ` - - - `; + function renderProfile() { + if (!current) return; + const { snapshot, user } = current; + const fields = fieldMap(snapshot); + const firstName = fields.first_name || ''; + const lastName = fields.last_name || ''; + const displayName = userDisplayName({ login, firstName, lastName }); + const avatar = snapshot?.avatar?.txId + ? { ar: String(snapshot.avatar.txId).trim(), sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase() } + : null; + const 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), + }; - const listWrap = document.createElement('div'); - listWrap.className = 'stack profile-param-list'; + body.innerHTML = ''; + const identity = document.createElement('div'); + identity.className = 'card row'; + identity.style.gap = '12px'; + identity.style.alignItems = 'center'; + identity.append(renderUserAvatar({ login, firstName, lastName, avatar, size: 'xl', className: 'profile-avatar' })); + const identityText = document.createElement('div'); + identityText.innerHTML = `
    ${escapeHtml(displayName)}
    `; + identity.append(identityText); + body.append(identity); - const accountRoleBtn = badgesRow.querySelector('[data-status="account_role"]'); - const shineBtn = badgesRow.querySelector('[data-status="shine"]'); - const identityEl = topRow.querySelector('[data-profile-identity="true"]'); - const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]'); + const badges = document.createElement('div'); + badges.innerHTML = statusBadges(String(snapshot?.accountRole || '').trim().toLowerCase(), String(snapshot?.shineStatus || '').trim().toLowerCase()); + body.append(...badges.children); - let currentFields = []; - let currentAccountRole = ''; - let currentShineStatus = ''; - let currentGender = 'unknown'; - let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; - let currentStats = { - ownedPublicChannelsCount: 0, - followingUsersCount: 0, - followingChannelsCount: 0, - closeFriendsCount: 0, - }; - - function syncIdentity() { - if (!identityEl) return; - const firstName = currentFields.find((field) => field.key === 'first_name')?.value || ''; - const lastName = currentFields.find((field) => field.key === 'last_name')?.value || ''; - const lines = buildIdentityLines({ login, firstName, lastName }); - identityEl.innerHTML = lines.map((line, idx) => ( - `
    ${escapeHtml(line)}
    ` - )).join(''); - } - - 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(); - avatarSlotEl.innerHTML = ''; - avatarSlotEl.append(renderUserAvatar({ - login, - firstName, - lastName, - avatar: currentAvatar?.txId - ? { ar: currentAvatar.txId, sha256Hex: String(currentAvatar?.sha256Hex || '').trim().toLowerCase() } - : null, - size: 'xl', - className: 'profile-avatar', - })); - } - - function updateStatusesUi() { - if (accountRoleBtn) { - const label = currentAccountRole === 'primary' ? 'Основной аккаунт' : currentAccountRole === 'non_voting' ? 'Не учитывать мой голос' : 'Не указано'; - accountRoleBtn.textContent = `Аккаунт: ${label}`; - accountRoleBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested'); - accountRoleBtn.classList.add(currentAccountRole === 'primary' ? 'is-yes-official' : 'is-no'); - } - if (shineBtn) { - const label = currentShineStatus === 'shining' ? 'Сияющий' : currentShineStatus === 'not_interested' ? 'Сияние неинтересно' : currentShineStatus === 'unknown' ? 'Неизвестно' : 'Не указано'; - shineBtn.textContent = `Сияние: ${label}`; - shineBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested'); - shineBtn.classList.add(currentShineStatus === 'shining' ? 'is-yes-shine' : currentShineStatus === 'not_interested' ? 'is-not-interested' : 'is-no'); + if (fields.about) { + const about = document.createElement('div'); + about.className = 'card profile-about'; + about.style.whiteSpace = 'pre-wrap'; + about.textContent = fields.about; + body.append(about); } + + const statsGrid = document.createElement('div'); + statsGrid.className = 'profile-stats-grid'; + statsRows(stats).forEach(([kind, label, value]) => { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'card profile-stat-card'; + button.dataset.profileList = kind; + button.innerHTML = `${Number(value || 0)}${escapeHtml(label)}`; + statsGrid.append(button); + }); + body.append(statsGrid); + + const detailRow = document.createElement('div'); + detailRow.className = 'row wrap-row'; + detailRow.innerHTML = ''; + body.append(detailRow); } - function renderStats() { - const stats = [ - { label: 'Собственные публичные каналы', value: currentStats.ownedPublicChannelsCount }, - { label: 'Подписки на пользователей', value: currentStats.followingUsersCount }, - { label: 'Подписки на каналы', value: currentStats.followingChannelsCount }, - { label: 'Близкие друзья', value: currentStats.closeFriendsCount }, - ]; - stats.forEach((stat) => { - const row = document.createElement('div'); - row.className = 'card profile-param-item row'; - row.innerHTML = `
    ${escapeHtml(stat.label)}: ${escapeHtml(String(Number(stat.value || 0)))}
    `; - listWrap.append(row); - }); - } - - accountRoleBtn?.classList.add('profile-badge-trigger'); - shineBtn?.classList.add('profile-badge-trigger'); - accountRoleBtn?.addEventListener('click', () => { - openProfileInfoModal({ - title: 'Основной аккаунт', - text: officialInfoText(), - }); - }); - shineBtn?.addEventListener('click', () => { - openProfileInfoModal({ - title: 'Справка о сияющих', - text: shineInfoText(), - }); - }); - - function renderFields(fields) { - listWrap.innerHTML = ''; - renderStats(); - fields.forEach((field) => { - const row = document.createElement('div'); - row.className = 'card profile-param-item row'; - const value = String(field.value || '').trim() || 'не заполнено'; - row.innerHTML = `
    ${field.label}: ${escapeHtml(value)}
    `; - listWrap.append(row); - - if (field.key === 'last_name') { - const genderRow = document.createElement('div'); - genderRow.className = 'card profile-param-item row'; - genderRow.innerHTML = `
    Пол: ${escapeHtml(genderLabel(currentGender))}
    `; - listWrap.append(genderRow); - } - }); - } - - async function refreshProfileSnapshot() { - if (state.session.isLocalDemo) { - currentFields = [ - { key: 'first_name', label: 'Имя', value: 'Тестовый' }, - { key: 'last_name', label: 'Фамилия', value: 'Пользователь' }, - { key: 'address', label: 'Адрес', value: 'Локальный режим' }, - { key: 'website', label: 'Веб', value: '127.0.0.1' }, - { key: 'phone', label: 'Телефон', value: profile.phone }, - ]; - currentAccountRole = ''; - currentShineStatus = ''; - currentGender = 'unknown'; - currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; - currentStats = { - ownedPublicChannelsCount: 0, - followingUsersCount: 0, - followingChannelsCount: 0, - closeFriendsCount: 0, - }; - syncIdentity(); - updateAvatarUi(); - updateStatusesUi(); - renderFields(currentFields); + body.addEventListener('click', (event) => { + if (!current) return; + const el = event.target.closest('[data-profile-list],[data-profile-detail]'); + if (!el) return; + const kind = el.dataset.profileList; + if (kind) { + navigate(`SHiNE/${encodeURIComponent(login)}/list/${encodeURIComponent(kind)}`); return; } - - try { - const [snapshot, user] = await Promise.all([ - loadProfileSnapshot(login), - authService.getUser(login).catch(() => ({})), - ]); - currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : []; - currentAccountRole = snapshot.accountRole || ''; - currentShineStatus = snapshot.shineStatus || ''; - currentGender = snapshot.gender || 'unknown'; - currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; - currentStats = { - ownedPublicChannelsCount: Number(user?.ownedPublicChannelsCount || 0), - followingUsersCount: Number(user?.followingUsersCount || 0), - followingChannelsCount: Number(user?.followingChannelsCount || 0), - closeFriendsCount: Number(user?.closeFriendsCount || 0), - }; - syncIdentity(); - updateAvatarUi(); - updateStatusesUi(); - renderFields(currentFields); - } catch (error) { - // ignore status row in profile-view + const fields = fieldMap(current.snapshot); + if (el.dataset.profileDetail === 'contacts') { + openTextModal('Контакты', [ + fields.web ? `Links: ${fields.web}` : '', + fields.phone ? `Телефон: ${fields.phone}` : '', + fields.address ? `Адрес: ${fields.address}` : '', + ].filter(Boolean).join('\n') || 'Не заполнено'); } + if (el.dataset.profileDetail === 'spiritual') openTextModal('Духовный путь', fields.spiritual_path); + }); + + async function refresh() { + if (state.session.isLocalDemo) { + status.textContent = 'Локальный тестовый режим.'; + return; + } + const [snapshot, user] = await Promise.all([loadProfileSnapshot(login), authService.getUser(login)]); + current = { snapshot, user }; + renderProfile(); + status.textContent = ''; } - card.append(topRow, badgesRow, listWrap); - screen.append(card); - - updateAvatarUi(); - refreshProfileSnapshot(); + refresh().catch((error) => { + status.className = 'status-line is-unavailable'; + status.textContent = `Ошибка: ${error?.message || 'unknown'}`; + }); screen.cleanup = () => profileMenu.destroy(); return screen; diff --git a/shine-UI/js/pages/user-profile-list-view.js b/shine-UI/js/pages/user-profile-list-view.js index cc7963b0..5c651e67 100644 --- a/shine-UI/js/pages/user-profile-list-view.js +++ b/shine-UI/js/pages/user-profile-list-view.js @@ -2,6 +2,7 @@ import { renderHeader } from '../components/header.js'; import { renderUserAvatar } from '../components/avatar-image.js'; import { authService } from '../state.js'; import { navigateBack } from '../router.js'; +import { userDisplayName } from '../services/user-display.js'; export const pageMeta = { id: 'user-profile-list-view', title: 'Список' }; @@ -47,9 +48,15 @@ export function render({ navigate, route }) { 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 fullName = userDisplayName(row); 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(' · '); + const marks = [ + row.relationType && row.relationType !== 'none' ? ({contact:'контакт',friend:'друг',close_friend:'близкий друг'}[row.relationType] || row.relationType) : '', + row.accountRole === 'primary' ? 'основной' : row.accountRole === 'non_voting' ? 'голос не учитывается' : '', + row.shineStatus === 'shining' ? 'сияющий' : row.shineStatus === 'not_interested' ? 'сияние неинтересно' : '', + row.primaryConfirmed ? 'основной подтверждён ✓' : '', + row.shineConfirmed ? 'сияющий подтверждён ✓' : '', + ].filter(Boolean).join(' · '); t.innerHTML = `${fullName}${String(row.login || '')}${marks ? ` · ${marks}` : ''}`; el.append(t); el.addEventListener('click', () => navigate(`SHiNE/${encodeURIComponent(row.login)}`)); body.append(el); }); diff --git a/shine-UI/js/services/user-display.js b/shine-UI/js/services/user-display.js new file mode 100644 index 00000000..d028dc9a --- /dev/null +++ b/shine-UI/js/services/user-display.js @@ -0,0 +1,6 @@ +export function userDisplayName({ login = '', firstName = '', lastName = '' } = {}) { + const first = String(firstName || '').trim(); + const last = String(lastName || '').trim(); + const full = [first, last].filter(Boolean).join(' ').trim(); + return full || String(login || '').trim() || 'unknown'; +} diff --git a/shine-UI/js/services/user-profile-params.js b/shine-UI/js/services/user-profile-params.js index 230d9bac..33dd57b1 100644 --- a/shine-UI/js/services/user-profile-params.js +++ b/shine-UI/js/services/user-profile-params.js @@ -10,7 +10,7 @@ export const profileFieldDefs = [ { key: 'first_name', readKeys: ['first_name'], label: 'Имя', placeholder: 'Введите имя' }, { key: 'last_name', readKeys: ['last_name'], label: 'Фамилия', placeholder: 'Введите фамилию' }, { key: 'address', readKeys: ['address'], label: 'Адрес', placeholder: 'Город, улица, дом' }, - { key: 'web', readKeys: ['web'], label: 'Веб', placeholder: 'Сайт или профиль' }, + { key: 'web', readKeys: ['web'], label: 'Links', placeholder: 'Сайт, профиль или другая ссылка' }, { key: 'phone', readKeys: ['phone'], label: 'Телефон', placeholder: '+7 ...' }, { key: 'about', readKeys: ['about'], label: 'О себе', placeholder: 'Коротко расскажите о себе', maxLength: 160, multiline: true }, { key: 'spiritual_path', readKeys: ['spiritual_path'], label: 'Духовный путь', placeholder: 'Расскажите о своём духовном пути, опыте, практиках и взглядах', maxLength: 5000, multiline: true }, diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 791a5c69..bd2dbf05 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -10683,3 +10683,8 @@ body.chat-topbar-overlay .composer-slot { rgba(0, 0, 0, 0.96) 100% ); } + +/* DM header: human name is primary, login remains secondary. */ +.chat-header-login-btn{display:flex;flex-direction:column;align-items:flex-start;line-height:1.1} +.chat-header-display-name{font-size:14px;font-weight:650} +.chat-header-user-login{font-size:11px;font-weight:400;opacity:.68;margin-top:2px} From 5d19e17a0bb6176837f3a807340ced30f8dead2aee6eef26627606285826f95b Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 1 Sep 2026 18:50:18 +0400 Subject: [PATCH 2/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D1=85=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 2 +- shine-UI/js/pages/messages-list.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION.properties b/VERSION.properties index 170f393a..7a5c2398 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.11.1 +client.version=1.11.2 server.version=1.9.0 diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index 6c4f8b90..d92fb8d1 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -22,6 +22,12 @@ const SVG_CHEVRON = ` `; +const RELATION_ORDER = new Map([ + ['close_friend', 0], + ['friend', 1], + ['contact', 2], + ['none', 99], +]); const DM_BLOB_PREVIEW_CACHE = new Map(); const DM_BLOB_PREVIEW_PENDING = new Map(); function normalizeRelationFlag(value) { From 6fcaa5d3d4f4f5b9033e55399444da605bc4c0b37ff1d836afd422ceb81037f0 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 1 Sep 2026 18:55:20 +0400 Subject: [PATCH 3/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=B2=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D1=85=20=D1=81=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 2 +- shine-UI/js/pages/chat-view.js | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index 7a5c2398..4b6936de 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.11.2 +client.version=1.11.3 server.version=1.9.0 diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index 71dd9512..219cde00 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -128,7 +128,7 @@ function createChatHeaderParts(login, navigate) { loginEl.setAttribute('role', 'heading'); loginEl.setAttribute('aria-level', '1'); loginEl.setAttribute('aria-label', `Чат с ${cleanLogin}`); - loginEl.innerHTML = `${cleanLogin}`; + loginEl.innerHTML = `${cleanLogin}`; void loadProfileSnapshot(cleanLogin) .then((snapshot) => { @@ -149,11 +149,19 @@ function createChatHeaderParts(login, navigate) { }); avatarSlot.replaceChildren(upgradedAvatar); if (loginEl.isConnected) { - const display = userDisplayName({ login: cleanLogin, firstName: snapshot?.firstName, lastName: snapshot?.lastName }); + const firstName = String(snapshot?.firstName || '').trim(); + const lastName = String(snapshot?.lastName || '').trim(); + const hasRealName = Boolean(firstName || lastName); + const display = userDisplayName({ login: cleanLogin, firstName, lastName }); const nameNode = loginEl.querySelector('.chat-header-display-name'); const loginNode = loginEl.querySelector('.chat-header-user-login'); - if (nameNode) nameNode.textContent = display; - if (loginNode) loginNode.textContent = cleanLogin; + if (nameNode) { + nameNode.textContent = hasRealName ? display : cleanLogin; + } + if (loginNode) { + loginNode.hidden = !hasRealName; + loginNode.textContent = hasRealName ? cleanLogin : ''; + } } }) .catch(() => {}); From f645686d87353b5166b484f9425408f7e5fae847b5e2350c2a46f3c98f949214 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 1 Sep 2026 19:00:05 +0400 Subject: [PATCH 4/4] =?UTF-8?q?Revert=20"=D0=98=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=20=D0=B2=20=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D1=85=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=D1=85"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 6fcaa5d3d4f4f5b9033e55399444da605bc4c0b37ff1d836afd422ceb81037f0. --- VERSION.properties | 2 +- shine-UI/js/pages/chat-view.js | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index 4b6936de..7a5c2398 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.11.3 +client.version=1.11.2 server.version=1.9.0 diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index 219cde00..71dd9512 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -128,7 +128,7 @@ function createChatHeaderParts(login, navigate) { loginEl.setAttribute('role', 'heading'); loginEl.setAttribute('aria-level', '1'); loginEl.setAttribute('aria-label', `Чат с ${cleanLogin}`); - loginEl.innerHTML = `${cleanLogin}`; + loginEl.innerHTML = `${cleanLogin}`; void loadProfileSnapshot(cleanLogin) .then((snapshot) => { @@ -149,19 +149,11 @@ function createChatHeaderParts(login, navigate) { }); avatarSlot.replaceChildren(upgradedAvatar); if (loginEl.isConnected) { - const firstName = String(snapshot?.firstName || '').trim(); - const lastName = String(snapshot?.lastName || '').trim(); - const hasRealName = Boolean(firstName || lastName); - const display = userDisplayName({ login: cleanLogin, firstName, lastName }); + const display = userDisplayName({ login: cleanLogin, firstName: snapshot?.firstName, lastName: snapshot?.lastName }); const nameNode = loginEl.querySelector('.chat-header-display-name'); const loginNode = loginEl.querySelector('.chat-header-user-login'); - if (nameNode) { - nameNode.textContent = hasRealName ? display : cleanLogin; - } - if (loginNode) { - loginNode.hidden = !hasRealName; - loginNode.textContent = hasRealName ? cleanLogin : ''; - } + if (nameNode) nameNode.textContent = display; + if (loginNode) loginNode.textContent = cleanLogin; } }) .catch(() => {});