SHA256
Обновить UI профиля и навигации
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { createTopBar } from '../components/topbar.js';
|
||||
import { authService, getMessageReactionState, setMessageReactionState, state } from '../state.js';
|
||||
import { captureClientError } from '../services/client-error-reporter.js';
|
||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||
import { extractLoginFromBlockchainName, makeProfileRoute, makeShineChannelRoute, makeShineMessageRoute } from '../services/shine-routes.js';
|
||||
|
||||
export const pageMeta = { id: 'channel-thread-view', title: 'Тред' };
|
||||
export const pageMeta = { id: 'channel-thread-view', title: 'Тред', shellMode: { scrollbar: 'hidden' } };
|
||||
const MSG_SUBTYPE_TEXT_POST = 10;
|
||||
const MSG_SUBTYPE_TEXT_RATING = 30;
|
||||
const MSG_SUBTYPE_TEXT_ENTRYPOINT = 100;
|
||||
@@ -437,7 +437,7 @@ function buildBlockchainDetails({ target, authorLogin, timestampMs, text, raw, l
|
||||
};
|
||||
}
|
||||
|
||||
function openBlockchainDetailsModal(details) {
|
||||
function openBlockchainDetailsModal(details, { isActive = () => true } = {}) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const rawText = JSON.stringify(details.raw || details, null, 2);
|
||||
root.innerHTML = `
|
||||
@@ -471,6 +471,7 @@ function openBlockchainDetailsModal(details) {
|
||||
});
|
||||
root.querySelector('#thread-blockchain-details-copy')?.addEventListener('click', async () => {
|
||||
await copyTextToClipboard(rawText);
|
||||
if (!isActive()) return;
|
||||
showToast('Данные блокчейна скопированы');
|
||||
});
|
||||
root.querySelector('#thread-blockchain-details-raw')?.addEventListener('click', () => {
|
||||
@@ -495,7 +496,7 @@ function renderDraftAttachments(container, attachments) {
|
||||
(Array.isArray(attachments) ? attachments : []).forEach((item, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'draft-attachment-chip';
|
||||
button.className = 'ui-button draft-attachment-chip';
|
||||
button.textContent = `${item.name} · ${item.ar}`;
|
||||
button.title = 'Нажмите, чтобы убрать вложение';
|
||||
button.addEventListener('click', () => {
|
||||
@@ -508,7 +509,7 @@ function renderDraftAttachments(container, attachments) {
|
||||
});
|
||||
}
|
||||
|
||||
function openReplyModal({ onSubmit, navigate, mode = 'reply' }) {
|
||||
function openReplyModal({ onSubmit, navigate, mode = 'reply', isActive = () => true }) {
|
||||
const isRating = mode === 'rating';
|
||||
const title = isRating ? 'Оценка' : 'Ответ';
|
||||
const placeholder = isRating ? 'Текст оценки' : 'Текст ответа';
|
||||
@@ -569,9 +570,11 @@ function openReplyModal({ onSubmit, navigate, mode = 'reply' }) {
|
||||
|
||||
try {
|
||||
await onSubmit(composeMessageWithAttachments(text, attachments));
|
||||
if (!isActive()) return;
|
||||
attachments.forEach((attachment) => markArweaveAttachmentPlaced(state.session.login, attachment));
|
||||
close();
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
setBusy(false);
|
||||
errorEl.textContent = toUserMessage(error, submitError);
|
||||
}
|
||||
@@ -589,10 +592,11 @@ function openReplyModal({ onSubmit, navigate, mode = 'reply' }) {
|
||||
gateway: state.entrySettings.arweaveServer,
|
||||
selectedTxIds: attachments.map((attachment) => attachment.ar),
|
||||
});
|
||||
if (!item) return;
|
||||
if (!isActive() || !item) return;
|
||||
attachments.push(item);
|
||||
renderDraftAttachments(attachmentsEl, attachments);
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось добавить вложение.');
|
||||
}
|
||||
});
|
||||
@@ -602,7 +606,7 @@ function openReplyModal({ onSubmit, navigate, mode = 'reply' }) {
|
||||
if (textEl) textEl.focus();
|
||||
}
|
||||
|
||||
function openRepostModal({ navigate, channels = [], onSubmit }) {
|
||||
function openRepostModal({ navigate, channels = [], onSubmit, isActive = () => true }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const options = (Array.isArray(channels) ? channels : [])
|
||||
.filter((item) => item?.selector?.ownerBlockchainName && Number.isFinite(Number(item?.selector?.channelRootBlockNumber)))
|
||||
@@ -668,8 +672,10 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
await onSubmit({ channel: channels[idx].selector, text });
|
||||
if (!isActive()) return;
|
||||
close();
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
setBusy(false);
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось сделать репост.');
|
||||
}
|
||||
@@ -716,7 +722,7 @@ function openMessageHistoryModal({ versions = [], title = 'История изм
|
||||
});
|
||||
}
|
||||
|
||||
function openEditMessageModal({ initialText = '', allowEmptyText = false, onSave, onDelete }) {
|
||||
function openEditMessageModal({ initialText = '', allowEmptyText = false, onSave, onDelete, isActive = () => true }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="thread-edit-modal">
|
||||
@@ -749,16 +755,20 @@ function openEditMessageModal({ initialText = '', allowEmptyText = false, onSave
|
||||
}
|
||||
try {
|
||||
await onSave(value);
|
||||
if (!isActive()) return;
|
||||
close();
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось изменить сообщение.');
|
||||
}
|
||||
});
|
||||
root.querySelector('#thread-edit-delete')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await onDelete();
|
||||
if (!isActive()) return;
|
||||
close();
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
|
||||
}
|
||||
});
|
||||
@@ -798,7 +808,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
|
||||
const authorTile = document.createElement('button');
|
||||
authorTile.type = 'button';
|
||||
authorTile.className = 'channel-message-author-tile';
|
||||
authorTile.className = 'ui-button channel-message-author-tile';
|
||||
const headRow = document.createElement('div');
|
||||
headRow.className = 'channel-message-head-row';
|
||||
|
||||
@@ -821,7 +831,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
if (versionsTotal > 1) {
|
||||
const editedMarker = document.createElement('button');
|
||||
editedMarker.type = 'button';
|
||||
editedMarker.className = 'message-edited-marker';
|
||||
editedMarker.className = 'ui-button message-edited-marker';
|
||||
editedMarker.textContent = `изменено ${Math.max(1, versionsTotal - 1)}`;
|
||||
editedMarker.title = 'Открыть историю редактирования';
|
||||
editedMarker.addEventListener('click', (event) => {
|
||||
@@ -844,7 +854,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
if (typeMeta) {
|
||||
const typeButton = document.createElement('button');
|
||||
typeButton.type = 'button';
|
||||
typeButton.className = 'channel-message-type-button';
|
||||
typeButton.className = 'ui-button channel-message-type-button';
|
||||
typeButton.textContent = typeMeta.label;
|
||||
if (typeMeta.actionable && typeof handlers?.onStatusAction === 'function') {
|
||||
typeButton.addEventListener('click', (event) => {
|
||||
@@ -865,7 +875,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
card.classList.add('channel-message-card--deleted-compact');
|
||||
const deleted = document.createElement('button');
|
||||
deleted.type = 'button';
|
||||
deleted.className = 'deleted-message-pill';
|
||||
deleted.className = 'ui-button deleted-message-pill';
|
||||
deleted.textContent = `Удалённое сообщение от ${author}`;
|
||||
deleted.title = 'Открыть историю изменений';
|
||||
deleted.addEventListener('click', (event) => {
|
||||
@@ -915,7 +925,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
|
||||
const likeButton = document.createElement('button');
|
||||
likeButton.type = 'button';
|
||||
likeButton.className = 'channel-action-item thread-like-btn';
|
||||
likeButton.className = 'ui-button channel-action-item thread-like-btn';
|
||||
if (isLiked) likeButton.classList.add('is-liked');
|
||||
likeButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">${isLiked ? '❤️' : '🤍'}</span>
|
||||
@@ -949,7 +959,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
|
||||
const replyButton = document.createElement('button');
|
||||
replyButton.type = 'button';
|
||||
replyButton.className = 'channel-action-item thread-reply-btn';
|
||||
replyButton.className = 'ui-button channel-action-item thread-reply-btn';
|
||||
replyButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">💬</span>
|
||||
<span class="channel-action-label">Ответить</span>
|
||||
@@ -962,6 +972,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
openReplyModal({
|
||||
navigate: handlers.navigate,
|
||||
onSubmit: async (textValue) => handlers.onReply(target, textValue),
|
||||
isActive: handlers.isActive,
|
||||
});
|
||||
});
|
||||
// Rating/opinion action is intentionally hidden from UI for now.
|
||||
@@ -969,7 +980,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
|
||||
const shareButton = document.createElement('button');
|
||||
shareButton.type = 'button';
|
||||
shareButton.className = 'channel-action-item thread-share-btn';
|
||||
shareButton.className = 'ui-button channel-action-item thread-share-btn';
|
||||
shareButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">↗</span>
|
||||
<span class="channel-action-label">Отправить</span>
|
||||
@@ -987,7 +998,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
if (repostTarget) {
|
||||
const originalButton = document.createElement('button');
|
||||
originalButton.type = 'button';
|
||||
originalButton.className = 'channel-action-item';
|
||||
originalButton.className = 'ui-button channel-action-item';
|
||||
originalButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">↪</span>
|
||||
<span class="channel-action-label">Оригинал</span>
|
||||
@@ -1009,7 +1020,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
}
|
||||
const detailsButton = document.createElement('button');
|
||||
detailsButton.type = 'button';
|
||||
detailsButton.className = 'channel-action-item';
|
||||
detailsButton.className = 'ui-button channel-action-item';
|
||||
detailsButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">⛓</span>
|
||||
<span class="channel-action-label">Данные блокчейна</span>
|
||||
@@ -1025,13 +1036,13 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
raw: node,
|
||||
localNumber,
|
||||
msgSubType,
|
||||
}));
|
||||
}), { isActive: handlers.isActive });
|
||||
});
|
||||
actions.append(detailsButton);
|
||||
if (isOwnMessage) {
|
||||
const editButton = document.createElement('button');
|
||||
editButton.type = 'button';
|
||||
editButton.className = 'channel-action-item';
|
||||
editButton.className = 'ui-button channel-action-item';
|
||||
editButton.setAttribute('aria-label', 'Редактировать');
|
||||
editButton.title = 'Редактировать';
|
||||
editButton.innerHTML = `
|
||||
@@ -1086,11 +1097,12 @@ function renderDescendants(items, handlers, nextNumber, depth = 0) {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function applyPendingScroll(screen, routeKey) {
|
||||
function applyPendingScroll(screen, routeKey, shouldContinue = () => true) {
|
||||
const target = pendingThreadScroll.get(routeKey);
|
||||
if (!target) return;
|
||||
|
||||
const doScroll = () => {
|
||||
if (!shouldContinue()) return;
|
||||
if (target === '__LAST_REPLY__') {
|
||||
const cards = screen.querySelectorAll('.thread-block--replies [data-message-key]');
|
||||
const last = cards[cards.length - 1];
|
||||
@@ -1108,7 +1120,7 @@ function applyPendingScroll(screen, routeKey) {
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(doScroll, 20);
|
||||
return window.setTimeout(doScroll, 20);
|
||||
}
|
||||
|
||||
function renderSkeleton(screen) {
|
||||
@@ -1120,15 +1132,18 @@ function renderSkeleton(screen) {
|
||||
}
|
||||
|
||||
export function render({ navigate, route, chrome }) {
|
||||
const selector = parseThreadSelector(route);
|
||||
let selector = parseThreadSelector(route);
|
||||
const channelDisplayName = resolveChannelDisplayName(selector?.channel);
|
||||
const routeKey = `${selector?.message?.blockchainName || ''}:${selector?.message?.blockNumber || ''}:${selector?.message?.blockHash || ''}`;
|
||||
let activeResolvedChannelLabel = channelDisplayName;
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack channels-screen channels-screen--thread';
|
||||
const appScreen = document.getElementById('app-screen');
|
||||
appScreen?.classList.add('channels-scroll-clean');
|
||||
|
||||
let disposed = false;
|
||||
let refreshSeq = 0;
|
||||
let refresh = () => {};
|
||||
const refreshTimers = new Set();
|
||||
|
||||
const threadHeaderButton = document.createElement('button');
|
||||
threadHeaderButton.type = 'button';
|
||||
@@ -1136,10 +1151,10 @@ export function render({ navigate, route, chrome }) {
|
||||
threadHeaderButton.textContent = 'Тред в канале: ...';
|
||||
threadHeaderButton.disabled = true;
|
||||
|
||||
const header = renderHeader({
|
||||
centerNode: threadHeaderButton,
|
||||
leftAction: { label: '<', onClick: () => navigate(resolveThreadPreviousInChannels(selector, activeResolvedChannelLabel)) },
|
||||
rightActions: [
|
||||
const header = createTopBar({
|
||||
center: threadHeaderButton,
|
||||
back: { label: '<', onClick: () => navigate(resolveThreadPreviousInChannels(selector, activeResolvedChannelLabel)) },
|
||||
actions: [
|
||||
{
|
||||
label: '↑',
|
||||
title: 'К списку каналов',
|
||||
@@ -1156,19 +1171,12 @@ export function render({ navigate, route, chrome }) {
|
||||
statusBox.className = 'card status-line is-unavailable channels-status';
|
||||
statusBox.style.display = 'none';
|
||||
|
||||
const rerender = () => {
|
||||
try {
|
||||
const current = document.querySelector('section.channels-screen--thread');
|
||||
if (!current) return;
|
||||
const next = render({ navigate, route });
|
||||
current.cleanup?.();
|
||||
current.replaceWith(next);
|
||||
} catch (error) {
|
||||
logThreadRuntimeError('rerender', error, { routePath: window.location.pathname });
|
||||
}
|
||||
const ensureActive = () => {
|
||||
if (disposed) throw new Error('Экран треда уже закрыт.');
|
||||
};
|
||||
|
||||
const showStatus = (message) => {
|
||||
if (disposed) return;
|
||||
if (!message) {
|
||||
statusBox.style.display = 'none';
|
||||
statusBox.textContent = '';
|
||||
@@ -1191,6 +1199,7 @@ export function render({ navigate, route, chrome }) {
|
||||
|
||||
const handlers = {
|
||||
navigate,
|
||||
isActive: () => !disposed,
|
||||
onToggleLike: async (target, action) => {
|
||||
const actionKey = makeReactionActionKey(target);
|
||||
if (!actionKey) throw new Error('Некорректная ссылка на сообщение для реакции.');
|
||||
@@ -1208,12 +1217,14 @@ export function render({ navigate, route, chrome }) {
|
||||
await authService.addBlockLike({ login, storagePwd, message: target });
|
||||
}
|
||||
|
||||
if (disposed) return;
|
||||
setMessageReactionState(target, nextReaction);
|
||||
softHaptic(10);
|
||||
rerender();
|
||||
void refresh();
|
||||
} catch (error) {
|
||||
if (disposed) return;
|
||||
setMessageReactionState(target, previousReaction || 'unliked');
|
||||
rerender();
|
||||
void refresh();
|
||||
throw error;
|
||||
} finally {
|
||||
pendingReactionActions.delete(actionKey);
|
||||
@@ -1222,24 +1233,27 @@ export function render({ navigate, route, chrome }) {
|
||||
onReply: async (target, textValue) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockReply({ login, storagePwd, message: target, text: textValue });
|
||||
ensureActive();
|
||||
pendingThreadScroll.set(routeKey, '__LAST_REPLY__');
|
||||
softHaptic(15);
|
||||
showToast('Ответ отправлен');
|
||||
showStatus('');
|
||||
rerender();
|
||||
void refresh();
|
||||
},
|
||||
onRating: async (target, textValue) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockRating({ login, storagePwd, message: target, text: textValue });
|
||||
ensureActive();
|
||||
pendingThreadScroll.set(routeKey, '__LAST_REPLY__');
|
||||
softHaptic(15);
|
||||
showToast('Оценка отправлена');
|
||||
showStatus('');
|
||||
rerender();
|
||||
void refresh();
|
||||
},
|
||||
onRepost: async (target) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
const feed = await authService.listSubscriptionsFeed(login, 1000);
|
||||
if (disposed) return;
|
||||
const channels = (Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [])
|
||||
.map((row) => {
|
||||
const selectorRow = {
|
||||
@@ -1264,6 +1278,7 @@ export function render({ navigate, route, chrome }) {
|
||||
openRepostModal({
|
||||
navigate,
|
||||
channels,
|
||||
isActive: () => !disposed,
|
||||
onSubmit: async ({ channel, text }) => {
|
||||
await authService.addBlockRepost({
|
||||
login,
|
||||
@@ -1272,6 +1287,7 @@ export function render({ navigate, route, chrome }) {
|
||||
message: target,
|
||||
text,
|
||||
});
|
||||
ensureActive();
|
||||
softHaptic(12);
|
||||
showToast('Репост опубликован');
|
||||
showStatus('');
|
||||
@@ -1287,6 +1303,7 @@ export function render({ navigate, route, chrome }) {
|
||||
text: 'Сообщение из треда SHiNE',
|
||||
url: buildAbsoluteRouteUrl(routePath),
|
||||
});
|
||||
if (disposed) return;
|
||||
if (result === 'copied') showToast('Ссылка скопирована');
|
||||
if (result === 'shared') showToast('Ссылка передана');
|
||||
if (result === 'copied' || result === 'shared') softHaptic(10);
|
||||
@@ -1320,33 +1337,69 @@ export function render({ navigate, route, chrome }) {
|
||||
isChannelPost: meta?.isChannelPost === true,
|
||||
channel: selector?.channel || null,
|
||||
});
|
||||
ensureActive();
|
||||
softHaptic(12);
|
||||
showToast('Сообщение обновлено');
|
||||
showStatus('');
|
||||
rerender();
|
||||
void refresh();
|
||||
},
|
||||
};
|
||||
|
||||
screen.append(statusBox);
|
||||
|
||||
if (!selector) {
|
||||
const invalid = document.createElement('div');
|
||||
invalid.className = 'card meta-muted';
|
||||
invalid.textContent = 'Некорректный идентификатор треда в адресе страницы.';
|
||||
screen.append(invalid);
|
||||
screen.cleanup = () => {
|
||||
appScreen?.classList.remove('channels-scroll-clean');
|
||||
};
|
||||
return screen;
|
||||
}
|
||||
const clearContent = () => {
|
||||
for (const timerId of refreshTimers) window.clearTimeout(timerId);
|
||||
refreshTimers.clear();
|
||||
Array.from(screen.children).forEach((child) => {
|
||||
if (child !== statusBox) child.remove();
|
||||
});
|
||||
};
|
||||
|
||||
const skeleton = renderSkeleton(screen);
|
||||
const clearOwnedModal = () => {
|
||||
const modalRoot = document.getElementById('modal-root');
|
||||
if (!modalRoot) return;
|
||||
if (modalRoot.querySelector([
|
||||
'#thread-blockchain-details-modal',
|
||||
'#thread-edit-modal',
|
||||
'#thread-history-modal',
|
||||
'#thread-reply-modal',
|
||||
'#thread-repost-modal',
|
||||
].join(','))) {
|
||||
modalRoot.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
const trackTimer = (timerId) => {
|
||||
if (timerId) refreshTimers.add(timerId);
|
||||
return timerId;
|
||||
};
|
||||
|
||||
refresh = async () => {
|
||||
if (disposed) return;
|
||||
const seq = ++refreshSeq;
|
||||
clearContent();
|
||||
showStatus('');
|
||||
selector = parseThreadSelector(route);
|
||||
activeResolvedChannelLabel = resolveChannelDisplayName(selector?.channel);
|
||||
threadHeaderButton.textContent = 'Тред в канале: ...';
|
||||
threadHeaderButton.disabled = true;
|
||||
threadHeaderButton.onclick = null;
|
||||
|
||||
if (!selector) {
|
||||
const invalid = document.createElement('div');
|
||||
invalid.className = 'card meta-muted';
|
||||
invalid.textContent = 'Некорректный идентификатор треда в адресе страницы.';
|
||||
screen.append(invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
const skeleton = renderSkeleton(screen);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
let resolvedMessage = selector.message;
|
||||
if (selector.short?.ownerBlockchainName && selector.short?.channelName) {
|
||||
const ownFeed = await authService.listSubscriptionsFeed(state.session.login, 1000);
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
const allRows = [
|
||||
...(Array.isArray(ownFeed?.ownedChannels) ? ownFeed.ownedChannels : []),
|
||||
...(Array.isArray(ownFeed?.followedUsersChannels) ? ownFeed.followedUsersChannels : []),
|
||||
@@ -1369,6 +1422,7 @@ export function render({ navigate, route, chrome }) {
|
||||
if (!channel && !looksLikeBlockchainName(ownerRaw)) {
|
||||
try {
|
||||
const ownerUser = await authService.getUser(ownerRaw);
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
const ownerBch = String(ownerUser?.blockchainName || '').trim().toLowerCase();
|
||||
if (ownerBch) {
|
||||
channel = allRows.find((item) => (
|
||||
@@ -1383,6 +1437,7 @@ export function render({ navigate, route, chrome }) {
|
||||
if (!channel && ownerLoginFromBch) {
|
||||
try {
|
||||
const ownerFeed = await authService.listSubscriptionsFeed(ownerLoginFromBch, 500);
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
const ownerRows = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels : [];
|
||||
channel = ownerRows.find((item) => (
|
||||
String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerNormalized
|
||||
@@ -1412,6 +1467,7 @@ export function render({ navigate, route, chrome }) {
|
||||
}
|
||||
|
||||
const payload = await authService.getMessageThread(resolvedMessage, 20, 2, 50, state.session.login);
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
skeleton.remove();
|
||||
|
||||
const ancestors = Array.isArray(payload?.ancestors) ? payload.ancestors : [];
|
||||
@@ -1437,6 +1493,7 @@ export function render({ navigate, route, chrome }) {
|
||||
let resolvedChannelLabel = resolveChannelDisplayName(selector?.channel);
|
||||
if (!resolvedChannelLabel && selector?.channel?.ownerBlockchainName && selector?.channel?.channelRootBlockNumber != null) {
|
||||
resolvedChannelLabel = await resolveChannelDisplayNameFromServer(selector.channel);
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
}
|
||||
activeResolvedChannelLabel = resolvedChannelLabel;
|
||||
const fallbackChannel = String(selector?.channel?.ownerBlockchainName || '').trim() || 'неизвестно';
|
||||
@@ -1453,10 +1510,10 @@ export function render({ navigate, route, chrome }) {
|
||||
};
|
||||
}
|
||||
|
||||
let seq = 0;
|
||||
let localSeq = 0;
|
||||
const nextNumber = () => {
|
||||
seq += 1;
|
||||
return seq;
|
||||
localSeq += 1;
|
||||
return localSeq;
|
||||
};
|
||||
|
||||
let ancestorsWrap = null;
|
||||
@@ -1506,25 +1563,33 @@ export function render({ navigate, route, chrome }) {
|
||||
if (focusWrap) screen.append(focusWrap);
|
||||
screen.append(descendantsWrap);
|
||||
|
||||
applyPendingScroll(screen, routeKey);
|
||||
trackTimer(applyPendingScroll(screen, routeKey, () => !disposed && seq === refreshSeq));
|
||||
const hasPendingScroll = pendingThreadScroll.has(routeKey);
|
||||
if (!hasPendingScroll && focusWrap) {
|
||||
setTimeout(() => {
|
||||
trackTimer(window.setTimeout(() => {
|
||||
if (disposed || seq !== refreshSeq || !focusWrap.isConnected) return;
|
||||
focusWrap.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
}, 20);
|
||||
}, 20));
|
||||
}
|
||||
} catch (error) {
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
skeleton.remove();
|
||||
const failed = document.createElement('div');
|
||||
failed.className = 'card meta-muted';
|
||||
failed.textContent = `Не удалось загрузить тред: ${toUserMessage(error, 'неизвестная ошибка')}`;
|
||||
screen.append(failed);
|
||||
}
|
||||
})();
|
||||
|
||||
screen.cleanup = () => {
|
||||
appScreen?.classList.remove('channels-scroll-clean');
|
||||
};
|
||||
|
||||
screen.refresh = refresh;
|
||||
screen.cleanup = () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
refreshSeq += 1;
|
||||
clearContent();
|
||||
clearOwnedModal();
|
||||
};
|
||||
|
||||
void refresh();
|
||||
return screen;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user