Files
SHiNE-server/shine-UI/js/pages/channel-about-view.js
T
qandClaude Opus 5.5 f8900e531a UI: новый общий стиль, тёмная/светлая тема, настраиваемая палитра
- styles/main.css: роли цветов (по умолчанию «Индиго»), шкала отступов/скруглений/шрифтов,
  цвета отношений для обеих тем; жёсткие цвета в стилях заменены на роли.
- Палитра: пресеты и личные правки, «Оформление» Авто/День/Ночь, долгое нажатие —
  редактор цветов с экспортом/импортом.
- Каналы: пузыри постов автора, плашки дней, строка «Написать в канал…», «О канале»,
  создание канала с адресом из названия; лента открывается на свежих постах.
- Чаты: плоский список, чипы-фильтры, пузыри, плашки дней; нижняя панель скрыта в переписке.
- Корневые разделы — единая шапка; профиль и чужой профиль — общая карточка;
  настройки, кошелёк, сеансы — меню-списки.
- Нижняя панель: иконки без подписей, бейджи на иконках.
- confirmDialog вместо window.confirm/alert; на телефоне диалоги — шторки снизу.
- docs/UI-Design/ISSUES-for-dev.md — найденные ошибки сервера/UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:05:26 +03:00

659 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { iconHtml } from '../components/ui-icon.js';
import { createTopBar } from '../components/topbar.js';
import { renderAvatar } from '../components/avatar-image.js';
import { authService, setChannelsFeed, state } from '../state.js';
import { navigateBack, getPreviousTrackedPath } from '../router.js';
import {
extractLoginFromBlockchainName,
makeProfileRoute,
makeShineChannelShortRoute,
makeShineChannelDonateRoute,
} from '../services/shine-routes.js';
import { loadProfileSnapshot } from '../services/user-profile-params.js';
import {
formatSol,
getBalanceSol,
getSolanaWalletFromStoredSecret,
getWalletFromStoredClientKey,
getWalletFromStoredRootKey,
solanaAddressFromPublicKeyBase64,
transferSol,
} from '../services/solana-wallet-service.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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function normalizeHash(hash) {
const normalized = String(hash || '').trim().toLowerCase();
return normalized || '0';
}
function toSafeInt(value) {
if (value === '' || value == null) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function buildChannelLink(route) {
if (!route) return '';
try {
return new URL(`/${String(route).replace(/^\/+/, '')}`, window.location.origin).toString();
} catch {
return `${window.location.origin}/${String(route).replace(/^\/+/, '')}`;
}
}
function statsText(value) {
return Number.isFinite(Number(value)) ? String(Math.max(0, Number(value))) : '0';
}
function profileField(snapshot, key) {
const row = Array.isArray(snapshot?.fields)
? snapshot.fields.find((item) => String(item?.key || '') === key)
: null;
return String(row?.value || '').trim();
}
function ownerDisplayName(snapshot, ownerLogin) {
const firstName = profileField(snapshot, 'first_name');
const lastName = profileField(snapshot, 'last_name');
return [firstName, lastName].filter(Boolean).join(' ').trim() || String(ownerLogin || '').trim() || 'Владелец канала';
}
function isSelectorMatch(row, selector) {
return Boolean(
row?.channel
&& String(row.channel.ownerBlockchainName || '') === String(selector?.ownerBlockchainName || '')
&& Number(row.channel.channelRoot?.blockNumber) === Number(selector?.channelRootBlockNumber)
&& normalizeHash(row.channel.channelRoot?.blockHash) === normalizeHash(selector?.channelRootBlockHash)
);
}
async function resolveChannelSelector(route) {
const ownerRef = String(route?.params?.ownerBlockchainName || '').trim();
const rootNo = toSafeInt(route?.params?.channelRootBlockNumber);
const rootHash = normalizeHash(route?.params?.channelRootBlockHash);
const channelName = String(route?.params?.channelName || '').trim();
if (ownerRef && rootNo != null) {
return {
ownerBlockchainName: ownerRef,
channelRootBlockNumber: rootNo,
channelRootBlockHash: rootHash,
channelName,
};
}
if (!ownerRef || !channelName) {
throw new Error('Не удалось определить канал из адреса страницы.');
}
const ownerLogin = extractLoginFromBlockchainName(ownerRef);
const ownerUser = await authService.getUser(ownerLogin);
if (!ownerUser?.exists) throw new Error('Владелец канала не найден.');
const ownerBlockchainName = String(ownerUser.blockchainName || ownerRef).trim();
const ownerFeed = await authService.listSubscriptionsFeed(ownerLogin, 500);
const ownedRows = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels : [];
const row = ownedRows.find((item) => (
String(item?.channel?.channelName || '').trim().toLowerCase() === channelName.toLowerCase()
&& String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBlockchainName.toLowerCase()
));
if (!row?.channel?.channelRoot) throw new Error('Канал не найден.');
return {
ownerBlockchainName,
channelRootBlockNumber: Number(row.channel.channelRoot.blockNumber),
channelRootBlockHash: normalizeHash(row.channel.channelRoot.blockHash),
channelName,
};
}
function publicKeyChoice(ownerUser, field, id, label) {
const publicKeyB64 = String(ownerUser?.[field] || '').trim();
if (!publicKeyB64) return null;
try {
return {
id,
label,
address: solanaAddressFromPublicKeyBase64(publicKeyB64),
};
} catch {
return null;
}
}
function openSupportChannelModal({ ownerLogin, ownerUser, channelTitle }) {
const root = document.getElementById('modal-root');
if (!root) return;
const recipientChoices = [
publicKeyChoice(ownerUser, 'solanaKey', 'root-key', 'Root key'),
publicKeyChoice(ownerUser, 'blockchainKey', 'blockchain-key', 'Blockchain key'),
publicKeyChoice(ownerUser, 'clientKey', 'client-key', 'Client key'),
].filter(Boolean);
root.innerHTML = `
<div class="modal" id="channel-support-modal">
<div class="modal-card stack channel-support-modal-card">
<div class="channel-support-modal-head">
<div>
<h3 class="modal-title">Донат автору</h3>
<p class="meta-muted channel-support-subtitle">${escapeHtml(channelTitle || 'Канал')} · @${escapeHtml(ownerLogin)}</p>
</div>
<button class="icon-btn channel-support-close" id="channel-support-close" type="button" aria-label="Закрыть" title="Закрыть">×</button>
</div>
<label class="field-label" for="channel-support-blockchain">Блокчейн</label>
<select class="select" id="channel-support-blockchain">
<option value="solana">Solana</option>
</select>
<label class="field-label" for="channel-support-recipient-key">Счёт получателя</label>
<select class="select" id="channel-support-recipient-key" ${recipientChoices.length ? '' : 'disabled'}>
${recipientChoices.map((choice) => `<option value="${escapeHtml(choice.id)}">${escapeHtml(choice.label)}</option>`).join('')}
</select>
<div class="channel-support-address" id="channel-support-recipient-address">—</div>
<label class="field-label" for="channel-support-sender-key">Перевести с моего счёта</label>
<select class="select" id="channel-support-sender-key">
<option value="client-key">Client key</option>
<option value="root-key">Root key</option>
</select>
<div class="channel-support-address" id="channel-support-sender-address">—</div>
<div class="channel-support-balance-row">
<span class="meta-muted">Баланс</span>
<strong id="channel-support-balance">—</strong>
<button class="secondary-btn channel-support-refresh" id="channel-support-refresh" type="button">Обновить</button>
</div>
<label class="field-label" for="channel-support-amount">Сумма</label>
<div class="channel-support-amount-row">
<input class="input" id="channel-support-amount" type="number" min="0" step="0.000001" inputmode="decimal" placeholder="0.1" />
<span>SOL</span>
</div>
<p class="meta-muted inline-error channel-support-status" id="channel-support-status"></p>
<div class="channel-support-result" id="channel-support-result" hidden></div>
<button class="primary-btn channel-support-submit" id="channel-support-submit" type="button" ${recipientChoices.length ? '' : 'disabled'}>Перевести</button>
</div>
</div>
`;
const modal = root.querySelector('#channel-support-modal');
const recipientSelect = root.querySelector('#channel-support-recipient-key');
const senderSelect = root.querySelector('#channel-support-sender-key');
const recipientAddressEl = root.querySelector('#channel-support-recipient-address');
const senderAddressEl = root.querySelector('#channel-support-sender-address');
const balanceEl = root.querySelector('#channel-support-balance');
const amountEl = root.querySelector('#channel-support-amount');
const statusEl = root.querySelector('#channel-support-status');
const resultEl = root.querySelector('#channel-support-result');
const submitEl = root.querySelector('#channel-support-submit');
const refreshEl = root.querySelector('#channel-support-refresh');
const walletCache = new Map();
let closed = false;
let busy = false;
const cleanupWallets = () => {
for (const wallet of walletCache.values()) {
try {
wallet?.keypair?.secretKey?.fill?.(0);
} catch {
// best effort: секрет существует только в памяти модального окна
}
}
walletCache.clear();
};
const close = () => {
if (closed) return;
closed = true;
cleanupWallets();
if (root.querySelector('#channel-support-modal') === modal) root.innerHTML = '';
};
const setBusy = (nextBusy) => {
busy = Boolean(nextBusy);
if (submitEl) submitEl.disabled = busy || recipientChoices.length === 0;
if (refreshEl) refreshEl.disabled = busy;
if (recipientSelect) recipientSelect.disabled = busy || recipientChoices.length === 0;
if (senderSelect) senderSelect.disabled = busy;
if (amountEl) amountEl.disabled = busy;
};
const setStatus = (message, kind = '') => {
if (!statusEl) return;
statusEl.textContent = String(message || '');
statusEl.classList.toggle('is-error', kind === 'error');
};
const selectedRecipient = () => recipientChoices.find((choice) => choice.id === String(recipientSelect?.value || '')) || recipientChoices[0] || null;
const updateRecipientAddress = () => {
const choice = selectedRecipient();
if (recipientAddressEl) recipientAddressEl.textContent = choice?.address || 'Публичный ключ получателя недоступен';
};
const resolveSenderWallet = async () => {
const keyId = String(senderSelect?.value || 'client-key');
if (walletCache.has(keyId)) return walletCache.get(keyId);
const login = String(state.session.login || '').trim();
const storagePwd = String(state.session.storagePwdInMemory || '').trim();
if (!login || !storagePwd) throw new Error('Для перевода нужно войти в Сиянии.');
let wallet;
if (keyId === 'root-key') {
try {
wallet = await getWalletFromStoredRootKey({ login, storagePwd });
} catch {
const password = window.prompt(
'Root key не сохранён на этом устройстве.\nВведите пароль аккаунта для временного восстановления Root key:',
'',
);
if (password == null) throw new Error('Операция отменена.');
const keyBundle = await authService.derivePasswordKeyBundle(login, password);
const rootPrivate = String(keyBundle?.rootPair?.privatePkcs8B64 || '').trim();
if (!rootPrivate) throw new Error('Не удалось временно восстановить Root key.');
wallet = await getSolanaWalletFromStoredSecret(rootPrivate);
}
} else {
wallet = await getWalletFromStoredClientKey({ login, storagePwd });
}
walletCache.set(keyId, wallet);
return wallet;
};
const refreshBalance = async () => {
if (busy || closed) return;
setBusy(true);
setStatus('Загрузка баланса…');
if (balanceEl) balanceEl.textContent = '—';
try {
const wallet = await resolveSenderWallet();
if (closed) return;
if (senderAddressEl) senderAddressEl.textContent = wallet.address;
const balance = await getBalanceSol({
endpoint: state.entrySettings.solanaServer,
address: wallet.address,
});
if (closed) return;
if (balanceEl) balanceEl.textContent = `${formatSol(balance.sol)} SOL`;
setStatus('');
} catch (error) {
if (closed) return;
if (senderAddressEl) senderAddressEl.textContent = '—';
setStatus(toUserMessage(error, 'Не удалось получить баланс.'), 'error');
} finally {
if (!closed) setBusy(false);
}
};
recipientSelect?.addEventListener('change', updateRecipientAddress);
senderSelect?.addEventListener('change', () => {
if (senderAddressEl) senderAddressEl.textContent = '—';
if (balanceEl) balanceEl.textContent = '—';
setStatus('');
void refreshBalance();
});
refreshEl?.addEventListener('click', () => void refreshBalance());
root.querySelector('#channel-support-close')?.addEventListener('click', close);
modal?.addEventListener('pointerdown', (event) => {
if (event.target === modal) close();
});
submitEl?.addEventListener('click', async () => {
if (busy || closed) return;
const recipient = selectedRecipient();
if (!recipient?.address) {
setStatus('Не удалось определить счёт получателя.', 'error');
return;
}
const amount = Number(String(amountEl?.value || '').replace(',', '.'));
if (!Number.isFinite(amount) || amount <= 0) {
setStatus('Введите сумму перевода больше 0.', 'error');
return;
}
setBusy(true);
setStatus('Отправляем перевод…');
if (resultEl) {
resultEl.hidden = true;
resultEl.innerHTML = '';
}
try {
const wallet = await resolveSenderWallet();
const result = await transferSol({
endpoint: state.entrySettings.solanaServer,
fromKeypair: wallet.keypair,
toAddress: recipient.address,
amountSol: amount,
});
if (closed) return;
setStatus('');
if (resultEl) {
resultEl.hidden = false;
resultEl.innerHTML = `
<strong>Перевод выполнен ✓</strong>
<span>${escapeHtml(formatSol(amount))} SOL → ${escapeHtml(recipient.label)}</span>
<code>${escapeHtml(String(result.signature || ''))}</code>
`;
}
showToast('Перевод отправлен');
const balance = await getBalanceSol({
endpoint: state.entrySettings.solanaServer,
address: wallet.address,
});
if (!closed && balanceEl) balanceEl.textContent = `${formatSol(balance.sol)} SOL`;
} catch (error) {
if (!closed) setStatus(toUserMessage(error, 'Не удалось выполнить перевод.'), 'error');
} finally {
if (!closed) setBusy(false);
}
});
updateRecipientAddress();
if (!recipientChoices.length) {
setStatus('У владельца канала нет доступных публичных ключей для перевода.', 'error');
} else {
void refreshBalance();
}
return close;
}
function confirmUnsubscribeModal({ channelTitle }) {
const root = document.getElementById('modal-root');
if (!root) return Promise.resolve(window.confirm('Отписаться от канала?'));
return new Promise((resolve) => {
let settled = false;
const finish = (value) => {
if (settled) return;
settled = true;
if (root.querySelector('#channel-unsubscribe-confirm-modal')) root.innerHTML = '';
resolve(Boolean(value));
};
root.innerHTML = `
<div class="modal" id="channel-unsubscribe-confirm-modal">
<div class="modal-card stack channel-unsubscribe-confirm-card">
<h3 class="modal-title">Отписаться от канала?</h3>
<p class="meta-muted channel-unsubscribe-confirm-text">
Вы действительно хотите отписаться от «${escapeHtml(channelTitle || 'этого канала')}»?
</p>
<div class="channel-unsubscribe-confirm-actions">
<button class="secondary-btn" id="channel-unsubscribe-no" type="button">Нет</button>
<button class="destructive-btn channel-unsubscribe-confirm-yes" id="channel-unsubscribe-yes" type="button">Да</button>
</div>
</div>
</div>
`;
const modal = root.querySelector('#channel-unsubscribe-confirm-modal');
root.querySelector('#channel-unsubscribe-no')?.addEventListener('click', () => finish(false));
root.querySelector('#channel-unsubscribe-yes')?.addEventListener('click', () => finish(true));
modal?.addEventListener('pointerdown', (event) => {
if (event.target === modal) finish(false);
});
});
}
function subscribersWord(count) {
const n = Math.abs(Number(count) || 0) % 100;
const last = n % 10;
if (n > 10 && n < 20) return 'подписчиков';
if (last === 1) return 'подписчик';
if (last >= 2 && last <= 4) return 'подписчика';
return 'подписчиков';
}
export function render({ navigate, route, chrome }) {
const screen = document.createElement('section');
screen.className = 'stack channels-screen channels-screen--channel-about';
const topbar = createTopBar({
title: 'О канале',
back: {
label: '←',
onClick: () => {
if (window.history.length > 1) {
navigateBack();
return;
}
navigate('channels-list');
},
ariaLabel: 'Назад',
title: 'Назад',
},
});
chrome?.setTopbar(topbar);
const card = document.createElement('div');
card.className = 'card stack channel-about-card';
const content = document.createElement('div');
content.className = 'stack channel-about-content';
content.innerHTML = '<div class="meta-muted">Загрузка данных канала…</div>';
card.append(content);
screen.append(card);
let disposed = false;
let refreshSeq = 0;
let closeSupportModal = null;
const requireSigningSession = () => {
const login = String(state.session.login || '').trim();
const storagePwd = String(state.session.storagePwdInMemory || '').trim();
if (!login || !storagePwd) {
state.authReturnHash = window.location.pathname || '/channels';
navigate('login-view');
throw new Error('Для этого действия нужно войти.');
}
return { login, storagePwd };
};
const renderContent = ({ channel, selector, ownerUser, ownerProfile, isOwnChannel, isSubscribed }) => {
const cleanName = String(channel?.displayTitle || channel?.displayName || channel?.channelName || 'Канал').trim();
const ownerLogin = String(channel?.ownerLogin || extractLoginFromBlockchainName(selector?.ownerBlockchainName) || '').trim();
const channelName = String(channel?.channelName || '').trim();
const description = String(channel?.channelDescription || channel?.description || '').trim();
const subscribersCount = Number(channel?.subscribersCount || 0);
const shortRoute = makeShineChannelShortRoute({
ownerLogin,
ownerBlockchainName: selector?.ownerBlockchainName,
channelName,
});
const donateRoute = makeShineChannelDonateRoute({
ownerBlockchainName: selector?.ownerBlockchainName,
channelRootBlockNumber: selector?.channelRootBlockNumber,
channelRootBlockHash: selector?.channelRootBlockHash,
});
const serverLink = buildChannelLink(shortRoute);
const shortLink = String(serverLink || '').replace(/^https?:\/\//, '');
const cameFromChannel = !!shortRoute && (getPreviousTrackedPath() || '').replace(/^\//, '') === String(shortRoute).replace(/^\//, '');
const ownerName = ownerDisplayName(ownerProfile, ownerLogin);
const subscriptionTile = isOwnChannel ? '' : `
<button class="channel-about-tile${isSubscribed ? ' is-active' : ''}" id="channel-about-subscription" type="button">
${iconHtml(isSubscribed ? 'check' : 'plus')}<span>${isSubscribed ? 'Вы подписаны' : 'Подписаться'}</span>
</button>`;
content.innerHTML = `
<div class="channel-about-hero">
<div class="channel-about-avatar-slot" id="channel-about-avatar-slot"></div>
<h2 class="channel-about-title">${escapeHtml(cleanName)}</h2>
<div class="channel-about-subline">@${escapeHtml(ownerLogin)} · ${escapeHtml(statsText(subscribersCount))} ${escapeHtml(subscribersWord(subscribersCount))}</div>
</div>
<div class="channel-about-tiles">
${cameFromChannel ? '' : `<button class="channel-about-tile" id="channel-about-open" type="button">${iconHtml('open')}<span>Открыть</span></button>`}
${subscriptionTile}
<button class="channel-about-tile" id="channel-about-support" type="button">${iconHtml('gift')}<span>Донат</span></button>
</div>
<div class="channel-about-list">
<div class="channel-about-row channel-about-row--static">
<span class="channel-about-row-label">Описание</span>
<p class="channel-about-description">${escapeHtml(description || 'Описание не задано.')}</p>
</div>
<button class="channel-about-row" id="channel-about-owner" type="button">
<span class="channel-about-owner-avatar" id="channel-about-owner-avatar"></span>
<span class="channel-about-row-main">
<strong>${escapeHtml(ownerName)}</strong>
<span class="channel-about-row-label">Владелец · @${escapeHtml(ownerLogin)}</span>
</span>
<span class="channel-about-row-chevron">${iconHtml('chevron')}</span>
</button>
<button class="channel-about-row" id="channel-about-copy-row" type="button">
<span class="channel-about-row-main">
<span class="channel-about-row-label">Ссылка</span>
<span class="channel-about-link">${escapeHtml(shortLink)}</span>
</span>
<span class="channel-about-row-chevron" aria-hidden="true">${iconHtml('link')}</span>
</button>
</div>
`;
const channelAvatar = renderAvatar({
initials: cleanName.slice(0, 1).toUpperCase() || 'К',
avatar: String(channel?.avaAr || '').trim() ? { ar: String(channel.avaAr || '').trim() } : null,
size: 'xl',
className: 'channel-about-avatar channel-profile-avatar',
title: cleanName,
alt: 'Аватар канала',
});
content.querySelector('#channel-about-avatar-slot')?.append(channelAvatar);
content.querySelector('#channel-about-owner-avatar')?.append(renderAvatar({
initials: ownerName.slice(0, 1).toUpperCase() || '?',
size: 'md',
className: 'channel-about-owner-avatar-img',
alt: '',
}));
content.querySelector('#channel-about-owner')?.addEventListener('click', () => {
const profileRoute = makeProfileRoute(ownerLogin);
if (profileRoute) navigate(profileRoute);
});
content.querySelector('#channel-about-support')?.addEventListener('click', () => {
if (donateRoute) navigate(donateRoute);
});
content.querySelector('#channel-about-open')?.addEventListener('click', () => {
if (shortRoute) navigate(shortRoute);
});
const copyLink = async () => {
if (!serverLink) return;
try {
await navigator.clipboard.writeText(serverLink);
showToast('Ссылка скопирована');
} catch (error) {
showToast(toUserMessage(error, 'Не удалось скопировать ссылку'), { kind: 'error' });
}
};
content.querySelector('#channel-about-copy-row')?.addEventListener('click', copyLink);
const subscriptionButton = content.querySelector('#channel-about-subscription');
subscriptionButton?.addEventListener('click', async () => {
if (subscriptionButton.disabled) return;
try {
const { login, storagePwd } = requireSigningSession();
if (isSubscribed) {
const confirmed = await confirmUnsubscribeModal({ channelTitle: cleanName });
if (!confirmed) return;
}
subscriptionButton.disabled = true;
await authService.addBlockFollowChannel({
login,
storagePwd,
targetBlockchainName: selector.ownerBlockchainName,
targetBlockNumber: selector.channelRootBlockNumber,
targetBlockHashHex: selector.channelRootBlockHash,
unfollow: isSubscribed,
});
if (disposed) return;
const feed = await authService.listSubscriptionsFeed(login, 1000);
if (disposed) return;
setChannelsFeed(feed, state.channelsIndex);
showToast(isSubscribed ? 'Вы отписались от канала' : 'Подписка на канал выполнена');
void refresh();
} catch (error) {
if (!disposed) {
showToast(toUserMessage(error, isSubscribed ? 'Не удалось отписаться от канала.' : 'Не удалось подписаться на канал.'), { kind: 'error' });
subscriptionButton.disabled = false;
}
}
});
};
const refresh = async () => {
const seq = ++refreshSeq;
content.innerHTML = '<div class="meta-muted">Загрузка данных канала…</div>';
try {
const selector = await resolveChannelSelector(route);
const payload = await authService.getChannelMessages(selector, 1, 'asc', String(state.session.login || '').trim());
if (disposed || seq !== refreshSeq) return;
const channel = payload?.channel || {};
const ownerLogin = String(channel?.ownerLogin || extractLoginFromBlockchainName(selector.ownerBlockchainName) || '').trim();
const currentLogin = String(state.session.login || '').trim();
const isOwnChannel = Boolean(ownerLogin && currentLogin && ownerLogin.toLowerCase() === currentLogin.toLowerCase());
const [ownerUserResult, ownerProfileResult, feedResult] = await Promise.allSettled([
authService.getUser(ownerLogin),
loadProfileSnapshot(ownerLogin),
currentLogin && !isOwnChannel ? authService.listSubscriptionsFeed(currentLogin, 1000) : Promise.resolve(null),
]);
if (disposed || seq !== refreshSeq) return;
const ownerUser = ownerUserResult.status === 'fulfilled' ? ownerUserResult.value : {};
const ownerProfile = ownerProfileResult.status === 'fulfilled' ? ownerProfileResult.value : null;
const feed = feedResult.status === 'fulfilled' ? feedResult.value : null;
if (feed) setChannelsFeed(feed, state.channelsIndex);
const followedRows = Array.isArray(feed?.followedChannels)
? feed.followedChannels
: (Array.isArray(state.channelsFeed?.followedChannels) ? state.channelsFeed.followedChannels : []);
const isSubscribed = !isOwnChannel && followedRows.some((row) => isSelectorMatch(row, selector));
renderContent({
channel,
selector,
ownerUser,
ownerProfile,
isOwnChannel,
isSubscribed,
});
} catch (error) {
if (disposed || seq !== refreshSeq) return;
content.innerHTML = `
<div class="meta-muted">Не удалось загрузить данные канала.</div>
<div class="meta-muted">${escapeHtml(toUserMessage(error, 'Проверьте подключение к серверу и повторите попытку.'))}</div>
<button class="secondary-btn" id="channel-about-retry" type="button">Повторить</button>
`;
content.querySelector('#channel-about-retry')?.addEventListener('click', () => void refresh());
}
};
screen.refresh = refresh;
screen.cleanup = () => {
if (disposed) return;
disposed = true;
refreshSeq += 1;
closeSupportModal?.();
closeSupportModal = null;
};
void refresh();
return screen;
}