SHA256
Добавили счётчик подписчиков в каналах и подписки пользователя
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user