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