SHA256
Добавили счётчик подписчиков в каналах и подписки пользователя
This commit is contained in:
@@ -93,6 +93,7 @@ import * as chatView from './pages/chat-view.js?v=202608221218';
|
||||
import * as userProfileView from './pages/user-profile-view.js';
|
||||
import * as channelsList from './pages/channels-list.js?v=202608221218';
|
||||
import * as channelView from './pages/channel-view.js';
|
||||
import * as channelAboutView from './pages/channel-about-view.js';
|
||||
import * as channelThreadView from './pages/channel-thread-view.js';
|
||||
import * as addChannelView from './pages/add-channel-view.js';
|
||||
import * as addPersonalPublicChatView from './pages/add-personal-public-chat-view.js';
|
||||
@@ -155,6 +156,7 @@ const routes = {
|
||||
user: userProfileView,
|
||||
'channels-list': channelsList,
|
||||
'channel-view': channelView,
|
||||
'channel-about-view': channelAboutView,
|
||||
'channel-thread-view': channelThreadView,
|
||||
'add-channel-view': addChannelView,
|
||||
'add-personal-public-chat-view': addPersonalPublicChatView,
|
||||
@@ -212,6 +214,7 @@ const GUEST_ALLOWED_PAGES = new Set([
|
||||
'network-view',
|
||||
'channels-list',
|
||||
'channel-view',
|
||||
'channel-about-view',
|
||||
'channel-thread-view',
|
||||
'user',
|
||||
'contact-search-view',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const SHINE_CONNECTIONS_LOGO_SRC = '/assets/SHiNE_connections_blue.svg';
|
||||
|
||||
export function createShineConnectionsLogo({ className = '' } = {}) {
|
||||
const img = document.createElement('img');
|
||||
img.src = SHINE_CONNECTIONS_LOGO_SRC;
|
||||
img.alt = '';
|
||||
img.setAttribute('aria-hidden', 'true');
|
||||
img.className = String(className || '').trim();
|
||||
return img;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveToolbarActive } from '../router.js';
|
||||
import { state } from '../state.js';
|
||||
import { openAuthRequiredModal } from '../services/auth-required-modal.js';
|
||||
import { SHINE_CONNECTIONS_LOGO_SRC } from './shine-logo.js';
|
||||
|
||||
// iconImg — путь к неоновой PNG (если есть, рисуем картинку вместо эмодзи); glow — цвет доп.свечения
|
||||
// активной/нажатой вкладки (var --tab-glow); hero — «герой»-вкладка (крупнее/ярче, всегда светится).
|
||||
@@ -8,7 +9,7 @@ import { openAuthRequiredModal } from '../services/auth-required-modal.js';
|
||||
const ITEMS = [
|
||||
{ pageId: 'messages-list', label: 'Личные', icon: '💬', iconImg: '/assets/icon_lichnye.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||
{ pageId: 'channels-list', label: 'Каналы', icon: '📢', iconImg: '/assets/icon_kanaly.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||
{ pageId: 'network-view', label: 'Связи', icon: '🕸', iconImg: '/assets/icon_svyazi.png', glow: 'rgba(0, 229, 255, .6)', hero: true },
|
||||
{ pageId: 'network-view', label: 'Связи', icon: '🕸', iconImg: SHINE_CONNECTIONS_LOGO_SRC, glow: 'rgba(0, 229, 255, .6)', hero: true },
|
||||
{ pageId: 'notifications-view', label: 'Уведомления', icon: '🔔', iconImg: '/assets/icon_uvedomleniya.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||
{ pageId: 'profile-view', label: 'Профиль', icon: '👤', iconImg: '/assets/icon_profil.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { authService, state } from '../state.js';
|
||||
import { navigateBack } from '../router.js';
|
||||
import { makeShineChannelRootRoute, makeShineChannelShortRoute } from '../services/shine-routes.js';
|
||||
import { showToast } from '../services/channels-ux.js';
|
||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
|
||||
export const pageMeta = { id: 'channel-about-view', title: 'О канале' };
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function normalizeHash(hash) {
|
||||
const normalized = String(hash || '').trim().toLowerCase();
|
||||
return normalized || '0';
|
||||
}
|
||||
|
||||
function toSafeInt(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function findCachedChannel(ownerBlockchainName, channelRootBlockNumber, channelRootBlockHash) {
|
||||
const ownerBch = String(ownerBlockchainName || '').trim().toLowerCase();
|
||||
const rootNo = Number(channelRootBlockNumber);
|
||||
const rootHash = normalizeHash(channelRootBlockHash);
|
||||
const rows = Object.values(state.channelsIndex || {});
|
||||
return rows.find((row) => (
|
||||
String(row?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBch
|
||||
&& Number(row?.channel?.channelRoot?.blockNumber) === rootNo
|
||||
&& normalizeHash(row?.channel?.channelRoot?.blockHash) === rootHash
|
||||
)) || null;
|
||||
}
|
||||
|
||||
function buildChannelLink(route) {
|
||||
if (!route) return '';
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = `/${String(route).replace(/^\/+/, '')}`;
|
||||
url.hash = '';
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function statsText(value) {
|
||||
return Number.isFinite(Number(value)) ? String(Math.max(0, Number(value))) : '0';
|
||||
}
|
||||
|
||||
export function render({ navigate, route, chrome }) {
|
||||
const ownerBlockchainName = String(route?.params?.ownerBlockchainName || '').trim();
|
||||
const channelRootBlockNumber = toSafeInt(route?.params?.channelRootBlockNumber);
|
||||
const channelRootBlockHash = normalizeHash(route?.params?.channelRootBlockHash);
|
||||
const channelRoute = makeShineChannelRootRoute({
|
||||
ownerBlockchainName,
|
||||
channelRootBlockNumber,
|
||||
channelRootBlockHash,
|
||||
});
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack channels-screen channels-screen--channel-about';
|
||||
|
||||
const topbar = renderHeader({
|
||||
title: 'О канале',
|
||||
leftAction: {
|
||||
label: '←',
|
||||
onClick: () => {
|
||||
if (window.history.length > 1) {
|
||||
navigateBack();
|
||||
return;
|
||||
}
|
||||
if (channelRoute) navigate(channelRoute);
|
||||
},
|
||||
ariaLabel: 'Назад',
|
||||
title: 'Назад',
|
||||
},
|
||||
});
|
||||
chrome?.setTopbar(topbar);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack channel-about-card';
|
||||
card.innerHTML = `
|
||||
<div class="stack" id="channel-about-content">
|
||||
<div class="meta-muted">Загрузка данных канала…</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'meta-muted screen-footer';
|
||||
footer.textContent = 'О канале (channel-about-view)';
|
||||
|
||||
screen.append(card, footer);
|
||||
|
||||
const renderContent = (channel) => {
|
||||
const cleanName = String(channel?.displayTitle || channel?.displayName || channel?.channelName || 'Канал').trim();
|
||||
const ownerName = String(channel?.ownerLogin || channel?.ownerName || 'автор').trim();
|
||||
const description = String(channel?.channelDescription || channel?.description || '').trim();
|
||||
const subscribersCount = Number(channel?.subscribersCount || 0);
|
||||
const aboutRoute = makeShineChannelRootRoute({
|
||||
ownerBlockchainName: channel?.ownerBlockchainName || ownerBlockchainName,
|
||||
channelRootBlockNumber: channel?.channelRoot?.blockNumber ?? channelRootBlockNumber,
|
||||
channelRootBlockHash: channel?.channelRoot?.blockHash ?? channelRootBlockHash,
|
||||
});
|
||||
const channelLinkRoute = makeShineChannelShortRoute({
|
||||
ownerBlockchainName: channel?.ownerBlockchainName || ownerBlockchainName,
|
||||
channelName: channel?.channelName || '',
|
||||
});
|
||||
const channelLink = buildChannelLink(channelLinkRoute);
|
||||
const changedAtMs = Number(channel?.metaUpdatedAtMs || 0);
|
||||
const changedAtLabel = changedAtMs ? new Date(changedAtMs).toLocaleString('ru-RU') : '—';
|
||||
const avatarState = String(channel?.avaAr || '').trim() ? 'Установлен' : 'Не установлен';
|
||||
|
||||
const content = card.querySelector('#channel-about-content');
|
||||
if (!content) return;
|
||||
content.innerHTML = `
|
||||
<div class="channel-profile-modal-head">
|
||||
<h2 class="modal-title">${escapeHtml(cleanName)}</h2>
|
||||
</div>
|
||||
<div class="channel-meta-details-grid">
|
||||
<span>Дата</span><strong>${escapeHtml(changedAtLabel)}</strong>
|
||||
<span>Владелец</span><strong>${escapeHtml(ownerName)}</strong>
|
||||
<span>Подписчиков</span><strong>${escapeHtml(statsText(subscribersCount))}</strong>
|
||||
<span>Системное имя</span><code>${escapeHtml(String(channel?.channelName || '').trim() || 'channel')}</code>
|
||||
<span>Название</span><strong>${escapeHtml(cleanName)}</strong>
|
||||
<span>Описание</span><span style="white-space: pre-wrap;">${escapeHtml(description || 'Описание не задано.')}</span>
|
||||
<span>Аватар</span><span>${escapeHtml(avatarState)}</span>
|
||||
<span>Ссылка</span><span><a href="${escapeHtml(channelLink)}">${escapeHtml(channelLink)}</a></span>
|
||||
</div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" type="button" id="channel-about-open">Открыть канал</button>
|
||||
<button class="secondary-btn" type="button" id="channel-about-copy">Скопировать ссылку</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
content.querySelector('#channel-about-open')?.addEventListener('click', () => {
|
||||
if (!channelLinkRoute) return;
|
||||
navigate(channelLinkRoute);
|
||||
});
|
||||
content.querySelector('#channel-about-copy')?.addEventListener('click', async () => {
|
||||
if (!channelLink) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(channelLink);
|
||||
showToast('Ссылка скопирована');
|
||||
} catch (error) {
|
||||
showToast(toUserMessage(error, 'Не удалось скопировать ссылку'), { kind: 'error' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cached = findCachedChannel(ownerBlockchainName, channelRootBlockNumber, channelRootBlockHash);
|
||||
if (cached?.channel) {
|
||||
renderContent({
|
||||
...cached.channel,
|
||||
subscribersCount: cached.channel.subscribersCount ?? cached.subscribersCount ?? 0,
|
||||
});
|
||||
return screen;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const payload = await authService.getChannelMessages({
|
||||
ownerBlockchainName,
|
||||
channelRootBlockNumber,
|
||||
channelRootBlockHash,
|
||||
}, 1, 'asc', String(state.session.login || '').trim());
|
||||
renderContent(payload?.channel || {});
|
||||
} catch (error) {
|
||||
const content = card.querySelector('#channel-about-content');
|
||||
if (content) {
|
||||
content.innerHTML = `
|
||||
<div class="meta-muted">Не удалось загрузить данные канала.</div>
|
||||
<div class="meta-muted">${escapeHtml(toUserMessage(error, 'Проверьте подключение к серверу и повторите попытку.'))}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return screen;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
extractLoginFromBlockchainName,
|
||||
makeProfileRoute,
|
||||
makeShineMessageRoute,
|
||||
makeShineChannelAboutRoute,
|
||||
} from '../services/shine-routes.js';
|
||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||
|
||||
@@ -2550,13 +2551,12 @@ export function render({ navigate, route, chrome }) {
|
||||
channelHeaderButton.disabled = false;
|
||||
channelHeaderButton.onclick = (event) => {
|
||||
animatePress(event.currentTarget);
|
||||
openAboutChannelModal(apiData.channel, {
|
||||
canEdit: apiData?.isOwnChannel === true && !apiData?.isDiary && !isStoriesChannel(apiData?.channel),
|
||||
onEdit: () => openEditChannelModal({
|
||||
channel: apiData.channel,
|
||||
onSave: onEditChannelMeta,
|
||||
}),
|
||||
const aboutRoute = makeShineChannelAboutRoute({
|
||||
ownerBlockchainName: apiData?.channel?.ownerBlockchainName || apiData?.selector?.ownerBlockchainName || '',
|
||||
channelRootBlockNumber: apiData?.selector?.channelRootBlockNumber ?? apiData?.channel?.channelRoot?.blockNumber ?? '',
|
||||
channelRootBlockHash: apiData?.selector?.channelRootBlockHash ?? apiData?.channel?.channelRoot?.blockHash ?? '',
|
||||
});
|
||||
if (aboutRoute) navigate(aboutRoute);
|
||||
};
|
||||
}
|
||||
if (channelEntrypointButton) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
writeChannelNotificationsState,
|
||||
} from '../services/channels-ux.js';
|
||||
import { makeShineChannelRoute } from '../services/shine-routes.js';
|
||||
import { makeShineChannelShortRoute } from '../services/shine-routes.js';
|
||||
import { renderAvatar } from '../components/avatar-image.js';
|
||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||
@@ -64,10 +65,9 @@ function buildChannelRouteFromSummary(summary, fallbackId) {
|
||||
const ownerBch = String(summary?.channel?.ownerBlockchainName || '').trim();
|
||||
const ownerLogin = String(summary?.channel?.ownerLogin || '').trim();
|
||||
const channelName = String(summary?.channel?.channelName || '').trim();
|
||||
return makeShineChannelRoute({
|
||||
ownerLogin,
|
||||
return makeShineChannelShortRoute({
|
||||
ownerBlockchainName: ownerBch,
|
||||
channelName: channelName || fallbackId,
|
||||
channelName: channelName || fallbackId || ownerLogin,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||
import { createShineConnectionsLogo } from '../components/shine-logo.js';
|
||||
import { directMessages } from '../mock-data.js';
|
||||
import {
|
||||
addAppLogEntry,
|
||||
@@ -30,11 +31,12 @@ 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 { makeProfileLinksRoute, makeProfileRoute } from '../services/shine-routes.js';
|
||||
|
||||
export const pageMeta = { id: 'chat-view', title: 'Чат' };
|
||||
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
||||
|
||||
function createChatHeaderParts(login) {
|
||||
function createChatHeaderParts(login, navigate) {
|
||||
const cleanLogin = String(login || '').trim() || 'unknown';
|
||||
|
||||
const avatarSlot = document.createElement('span');
|
||||
@@ -47,6 +49,13 @@ function createChatHeaderParts(login) {
|
||||
});
|
||||
avatarSlot.append(initialAvatar);
|
||||
|
||||
const avatarButton = document.createElement('button');
|
||||
avatarButton.type = 'button';
|
||||
avatarButton.className = 'chat-header-avatar-btn';
|
||||
avatarButton.title = `Профиль ${cleanLogin}`;
|
||||
avatarButton.setAttribute('aria-label', `Открыть профиль ${cleanLogin}`);
|
||||
avatarButton.append(avatarSlot);
|
||||
|
||||
const loginEl = document.createElement('span');
|
||||
loginEl.className = 'chat-header-login';
|
||||
loginEl.setAttribute('role', 'heading');
|
||||
@@ -75,7 +84,23 @@ function createChatHeaderParts(login) {
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return { centerNode: loginEl, avatarSlot };
|
||||
const connectionsButton = document.createElement('button');
|
||||
connectionsButton.type = 'button';
|
||||
connectionsButton.className = 'icon-btn chat-header-icon-btn chat-header-connections-btn';
|
||||
connectionsButton.title = `Связи ${cleanLogin}`;
|
||||
connectionsButton.setAttribute('aria-label', `Открыть связи ${cleanLogin}`);
|
||||
connectionsButton.append(createShineConnectionsLogo({ className: 'chat-header-connections-logo' }));
|
||||
connectionsButton.addEventListener('click', () => {
|
||||
if (!cleanLogin || cleanLogin === 'unknown') return;
|
||||
navigate(makeProfileLinksRoute(cleanLogin));
|
||||
});
|
||||
|
||||
avatarButton.addEventListener('click', () => {
|
||||
if (!cleanLogin || cleanLogin === 'unknown') return;
|
||||
navigate(makeProfileRoute(cleanLogin));
|
||||
});
|
||||
|
||||
return { centerNode: loginEl, avatarButton, connectionsButton };
|
||||
}
|
||||
|
||||
function truncatePreviewText(value, maxLen = 72) {
|
||||
@@ -293,7 +318,6 @@ function openChatActionsMenu({
|
||||
anchorY = 0,
|
||||
onCall,
|
||||
onVideoCall,
|
||||
onInstantVideoCall,
|
||||
onClearHistory,
|
||||
onDeleteChat,
|
||||
}) {
|
||||
@@ -305,8 +329,7 @@ function openChatActionsMenu({
|
||||
<div class="dm-floating-menu-layer" id="chat-header-actions-layer">
|
||||
<div class="modal-card stack dm-message-actions-menu dm-message-actions-popover" id="${menuId}">
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-call">Звонок</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-video-call">Звонок с поддержкой видео</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-instant-video-call">Видеозвонок</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-video-call">Видеозвонок</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-clear-history">Очистить историю</button>
|
||||
<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="chat-menu-delete-chat">Удалить чат</button>
|
||||
</div>
|
||||
@@ -358,10 +381,6 @@ function openChatActionsMenu({
|
||||
close();
|
||||
if (typeof onVideoCall === 'function') await onVideoCall();
|
||||
});
|
||||
root.querySelector('#chat-menu-instant-video-call')?.addEventListener('click', async () => {
|
||||
close();
|
||||
if (typeof onInstantVideoCall === 'function') await onInstantVideoCall();
|
||||
});
|
||||
root.querySelector('#chat-menu-clear-history')?.addEventListener('click', async () => {
|
||||
close();
|
||||
if (typeof onClearHistory === 'function') await onClearHistory();
|
||||
@@ -993,7 +1012,7 @@ export function render({ navigate, route, chrome }) {
|
||||
const log = document.createElement('div');
|
||||
log.className = 'messages-log dm-messages-log';
|
||||
|
||||
const chatHeaderParts = createChatHeaderParts(chatId);
|
||||
const chatHeaderParts = createChatHeaderParts(chatId, navigate);
|
||||
const chatHeader = renderHeader({
|
||||
centerNode: chatHeaderParts.centerNode,
|
||||
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
||||
@@ -1016,7 +1035,6 @@ export function render({ navigate, route, chrome }) {
|
||||
anchorY: Number(event?.currentTarget?.getBoundingClientRect?.().bottom || event?.clientY || 0),
|
||||
onCall: () => handleStartCall('audio'),
|
||||
onVideoCall: () => handleStartCall('video'),
|
||||
onInstantVideoCall: () => handleStartCall('instant_video'),
|
||||
onClearHistory: async () => {
|
||||
openChatConfirmModal({
|
||||
title: 'Очистить историю?',
|
||||
@@ -1064,7 +1082,8 @@ export function render({ navigate, route, chrome }) {
|
||||
},
|
||||
],
|
||||
});
|
||||
chatHeader.querySelector('.header-left')?.append(chatHeaderParts.avatarSlot);
|
||||
const chatHeaderLeft = chatHeader.querySelector('.header-left');
|
||||
chatHeaderLeft?.append(chatHeaderParts.connectionsButton, chatHeaderParts.avatarButton);
|
||||
chrome?.setTopbar(chatHeader);
|
||||
|
||||
if (!isKnownContact) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../state.js';
|
||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||
import { createShineConnectionsLogo } from '../components/shine-logo.js';
|
||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||
|
||||
@@ -167,15 +168,11 @@ function compareChatRows(a, b) {
|
||||
export function render({ navigate, chrome }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack dm-screen dm-list-screen';
|
||||
const login = String(state.session.login || '').trim();
|
||||
const head = document.createElement('header');
|
||||
head.className = 'dm-head';
|
||||
head.innerHTML = `
|
||||
<div class="dm-head-brand">
|
||||
<div class="dm-head-hex">${(login[0] || 'A').toUpperCase()}</div>
|
||||
<div class="dm-head-id">
|
||||
<span class="dm-head-name"></span>
|
||||
</div>
|
||||
<span class="dm-head-logo-wrap" aria-hidden="true"></span>
|
||||
</div>
|
||||
<h1 class="dm-head-title">Чаты</h1>
|
||||
<div class="dm-head-menu-wrap">
|
||||
@@ -191,6 +188,9 @@ export function render({ navigate, chrome }) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
head.querySelector('.dm-head-logo-wrap')?.append(
|
||||
createShineConnectionsLogo({ className: 'dm-head-logo' }),
|
||||
);
|
||||
const menuButton = head.querySelector('.dm-head-menu-btn');
|
||||
menuButton?.append(createOverflowDots());
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { profile } from '../mock-data.js';
|
||||
import { state } from '../state.js';
|
||||
import { authService, state } from '../state.js';
|
||||
import {
|
||||
PROFILE_GENDER_FEMALE,
|
||||
PROFILE_GENDER_MALE,
|
||||
@@ -223,6 +223,12 @@ export function render({ navigate, chrome }) {
|
||||
let currentToggles = [];
|
||||
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;
|
||||
@@ -269,6 +275,21 @@ export function render({ navigate, chrome }) {
|
||||
updateToggleButton(shineBtn, 'Сияющий', shine.enabled);
|
||||
}
|
||||
|
||||
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 = `<div class="profile-param-value"><b>${escapeHtml(stat.label)}</b>: ${escapeHtml(String(Number(stat.value || 0)))}</div>`;
|
||||
listWrap.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
officialBtn?.classList.add('profile-badge-trigger');
|
||||
shineBtn?.classList.add('profile-badge-trigger');
|
||||
officialBtn?.addEventListener('click', () => {
|
||||
@@ -286,6 +307,7 @@ export function render({ navigate, chrome }) {
|
||||
|
||||
function renderFields(fields) {
|
||||
listWrap.innerHTML = '';
|
||||
renderStats();
|
||||
fields.forEach((field) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'card profile-param-item row';
|
||||
@@ -317,6 +339,12 @@ export function render({ navigate, chrome }) {
|
||||
];
|
||||
currentGender = 'unknown';
|
||||
currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
currentStats = {
|
||||
ownedPublicChannelsCount: 0,
|
||||
followingUsersCount: 0,
|
||||
followingChannelsCount: 0,
|
||||
closeFriendsCount: 0,
|
||||
};
|
||||
syncIdentity();
|
||||
updateAvatarUi();
|
||||
updateTogglesUi();
|
||||
@@ -325,11 +353,20 @@ export function render({ navigate, chrome }) {
|
||||
}
|
||||
|
||||
try {
|
||||
const snapshot = await loadProfileSnapshot(login);
|
||||
const [snapshot, user] = await Promise.all([
|
||||
loadProfileSnapshot(login),
|
||||
authService.getUser(login).catch(() => ({})),
|
||||
]);
|
||||
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
|
||||
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
|
||||
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();
|
||||
updateTogglesUi();
|
||||
|
||||
+45
-1
@@ -26,6 +26,7 @@ const PRETTY_PATHS = new Map([
|
||||
['add-channel-view', 'channels/new'],
|
||||
['add-personal-public-chat-view', 'channels/new-public-chat'],
|
||||
['channel-view', 'channel'],
|
||||
['channel-about-view', 'channel/about'],
|
||||
['channel-thread-view', 'thread'],
|
||||
['network-view', 'network'],
|
||||
['notifications-view', 'notifications'],
|
||||
@@ -52,6 +53,10 @@ const PRETTY_PATHS = new Map([
|
||||
['remote-addblock-session-view', 'remote-addblock-session'],
|
||||
]);
|
||||
|
||||
function looksLikeBlockchainName(value) {
|
||||
return /^.+-\d+$/.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
export const PRE_AUTH_PAGES = [
|
||||
'start-view',
|
||||
'entry-settings-view',
|
||||
@@ -162,6 +167,34 @@ export function parseRouteFromPath(pathname = '') {
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.length >= 2 && looksLikeBlockchainName(segments[0])) {
|
||||
const ownerBlockchainName = decodePart(segments[0]);
|
||||
const channelName = decodePart(segments[1] || '');
|
||||
const sub = decodePart(segments[2] || '').toLowerCase();
|
||||
if (ownerBlockchainName && channelName) {
|
||||
if (sub === 'about') {
|
||||
return {
|
||||
pageId: 'channel-about-view',
|
||||
params: {
|
||||
ownerBlockchainName,
|
||||
channelRootBlockNumber: '',
|
||||
channelRootBlockHash: '',
|
||||
channelId: '',
|
||||
channelName,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
pageId: 'channel-view',
|
||||
params: {
|
||||
ownerBlockchainName,
|
||||
channelName,
|
||||
channelId: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (pageId === 'chat' || pageId === 'chat-view') {
|
||||
return { pageId: 'chat-view', params: { chatId: dynamicId ? decodeURIComponent(dynamicId) : '' } };
|
||||
}
|
||||
@@ -225,6 +258,17 @@ export function parseRouteFromPath(pathname = '') {
|
||||
}
|
||||
|
||||
if (pageId === 'channel') {
|
||||
if (segments.length >= 5 && decodePart(segments[4] || '').toLowerCase() === 'about') {
|
||||
return {
|
||||
pageId: 'channel-about-view',
|
||||
params: {
|
||||
ownerBlockchainName: decodePart(segments[1]),
|
||||
channelRootBlockNumber: segments[2] || '',
|
||||
channelRootBlockHash: segments[3] || '',
|
||||
channelId: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (segments.length >= 4) {
|
||||
return {
|
||||
pageId: 'channel-view',
|
||||
@@ -408,7 +452,7 @@ export function resolveToolbarActive(pageId) {
|
||||
pageId === 'solana-users-init-view'
|
||||
) return 'profile-view';
|
||||
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
|
||||
if (pageId === 'channel-view' || pageId === 'channel-thread-view' || pageId === 'add-channel-view' || pageId === 'add-personal-public-chat-view') return 'channels-list';
|
||||
if (pageId === 'channel-view' || pageId === 'channel-about-view' || pageId === 'channel-thread-view' || pageId === 'add-channel-view' || pageId === 'add-personal-public-chat-view') return 'channels-list';
|
||||
if (pageId === 'user') return 'messages-list';
|
||||
return 'profile-view';
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ function getCallTitleText(mode) {
|
||||
return 'Видеозвонок';
|
||||
}
|
||||
if (normalizeCallMode(mode) === CALL_MODE_VIDEO) {
|
||||
return 'Звонок с поддержкой видео';
|
||||
return 'Видеозвонок';
|
||||
}
|
||||
return 'Звонок';
|
||||
}
|
||||
@@ -164,7 +164,7 @@ function getIncomingCallStatusText(peerLogin, mode) {
|
||||
return `Входящий видеозвонок от ${name}`;
|
||||
}
|
||||
if (isVideoCallMode(mode)) {
|
||||
return `Вам звонит ${name} (звонок с поддержкой видео)`;
|
||||
return `Входящий видеозвонок от ${name}`;
|
||||
}
|
||||
return `Вам звонит ${name}`;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,26 @@ export function makeShineChannelRoute({ ownerLogin = '', ownerBlockchainName = '
|
||||
return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}`;
|
||||
}
|
||||
|
||||
export function makeShineChannelRootRoute({ ownerBlockchainName = '', channelRootBlockNumber = '', channelRootBlockHash = '' }) {
|
||||
const ownerBch = String(ownerBlockchainName || '').trim();
|
||||
const rootNo = String(channelRootBlockNumber || '').trim();
|
||||
const rootHash = String(channelRootBlockHash || '').trim();
|
||||
if (!ownerBch || !rootNo || !rootHash) return '';
|
||||
return `channel/${encodeRoutePart(ownerBch)}/${encodeRoutePart(rootNo)}/${encodeRoutePart(rootHash)}`;
|
||||
}
|
||||
|
||||
export function makeShineChannelAboutRoute({ ownerBlockchainName = '', channelRootBlockNumber = '', channelRootBlockHash = '' }) {
|
||||
const base = makeShineChannelRootRoute({ ownerBlockchainName, channelRootBlockNumber, channelRootBlockHash });
|
||||
return base ? `${base}/about` : '';
|
||||
}
|
||||
|
||||
export function makeShineChannelShortRoute({ ownerBlockchainName = '', channelName = '' }) {
|
||||
const ownerBch = String(ownerBlockchainName || '').trim();
|
||||
const chName = String(channelName || '').trim();
|
||||
if (!ownerBch || !chName) return '';
|
||||
return `${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}`;
|
||||
}
|
||||
|
||||
export function makeShineMessageRoute({ ownerLogin = '', messageBlockchainName = '', messageBlockNumber = '' }) {
|
||||
const msgBch = String(messageBlockchainName || '').trim();
|
||||
const msgNo = String(messageBlockNumber || '').trim();
|
||||
|
||||
Reference in New Issue
Block a user