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(focus ? (focus.name || focus.login || '') : '')}. Связей 1-го уровня: ${tier1.length}.