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,
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from '../services/shine-routes.js';
|
||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||
|
||||
export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
||||
export const pageMeta = { id: 'channel-view', title: 'Канал', shellMode: { scrollbar: 'hidden' } };
|
||||
const CHANNEL_TYPE_STORIES = 0;
|
||||
const CHANNEL_TYPE_PERSONAL = 100;
|
||||
const MSG_SUBTYPE_TEXT_RATING = 30;
|
||||
@@ -775,7 +775,7 @@ function buildBlockchainDetails({ messageRef, authorLogin, timestampMs, text, ra
|
||||
};
|
||||
}
|
||||
|
||||
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 = `
|
||||
@@ -809,6 +809,7 @@ function openBlockchainDetailsModal(details) {
|
||||
});
|
||||
root.querySelector('#blockchain-details-copy')?.addEventListener('click', async () => {
|
||||
await copyTextToClipboard(rawText);
|
||||
if (!isActive()) return;
|
||||
showToast('Данные блокчейна скопированы');
|
||||
});
|
||||
root.querySelector('#blockchain-details-raw')?.addEventListener('click', () => {
|
||||
@@ -824,7 +825,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', () => {
|
||||
@@ -837,7 +838,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 ? 'Текст оценки' : 'Текст ответа';
|
||||
@@ -898,9 +899,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);
|
||||
}
|
||||
@@ -918,10 +921,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, 'Не удалось добавить вложение.');
|
||||
}
|
||||
});
|
||||
@@ -931,7 +935,7 @@ function openReplyModal({ onSubmit, navigate, mode = 'reply' }) {
|
||||
if (textEl) textEl.focus();
|
||||
}
|
||||
|
||||
function openStatusActionCommentModal({ title, submitLabel, onSubmit }) {
|
||||
function openStatusActionCommentModal({ title, submitLabel, onSubmit, isActive = () => true }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="channel-status-action-modal">
|
||||
@@ -973,8 +977,10 @@ function openStatusActionCommentModal({ title, submitLabel, onSubmit }) {
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
await onSubmit(String(textEl?.value || '').trim());
|
||||
if (!isActive()) return;
|
||||
close();
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
setBusy(false);
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось сохранить действие.');
|
||||
}
|
||||
@@ -984,7 +990,7 @@ function openStatusActionCommentModal({ title, submitLabel, onSubmit }) {
|
||||
if (textEl) textEl.focus();
|
||||
}
|
||||
|
||||
function openStatusActionMenuModal({ targetLabel, options = [], onSelect }) {
|
||||
function openStatusActionMenuModal({ targetLabel, options = [], onSelect, isActive = () => true }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const rows = (Array.isArray(options) ? options : [])
|
||||
.map((item, index) => `
|
||||
@@ -1017,6 +1023,7 @@ function openStatusActionMenuModal({ targetLabel, options = [], onSelect }) {
|
||||
const option = options[idx];
|
||||
if (!option) return;
|
||||
close();
|
||||
if (!isActive()) return;
|
||||
await onSelect(option);
|
||||
});
|
||||
});
|
||||
@@ -1042,8 +1049,12 @@ function flashAndScrollToMessage(messageRef) {
|
||||
if (!target) return false;
|
||||
target.classList.remove('is-focus-flash');
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
window.setTimeout(() => target.classList.add('is-focus-flash'), 60);
|
||||
window.setTimeout(() => target.classList.remove('is-focus-flash'), 1800);
|
||||
window.setTimeout(() => {
|
||||
if (target.isConnected) target.classList.add('is-focus-flash');
|
||||
}, 60);
|
||||
window.setTimeout(() => {
|
||||
if (target.isConnected) target.classList.remove('is-focus-flash');
|
||||
}, 1800);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1097,7 +1108,7 @@ function openEntrypointHistoryModal({ channelTitle = '', posts = [], onSelect })
|
||||
const parsed = parseMessageAttachments(post.body);
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'entrypoint-history-item';
|
||||
item.className = 'ui-button entrypoint-history-item';
|
||||
item.innerHTML = `
|
||||
<strong>${escapeHtml(post.timestampMs ? new Date(post.timestampMs).toLocaleString('ru-RU') : 'Без даты')}</strong>
|
||||
<span>#${escapeHtml(post.localNumber || '—')}</span>
|
||||
@@ -1116,7 +1127,7 @@ function openEntrypointHistoryModal({ channelTitle = '', posts = [], onSelect })
|
||||
});
|
||||
}
|
||||
|
||||
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)))
|
||||
@@ -1180,8 +1191,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, 'Не удалось сделать репост.');
|
||||
}
|
||||
@@ -1190,7 +1203,7 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
|
||||
if (textEl) textEl.focus();
|
||||
}
|
||||
|
||||
function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
function openAddMessageModal({ channelName, onSubmit, navigate, isActive = () => true }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="channel-message-modal">
|
||||
@@ -1258,9 +1271,11 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
text: composeMessageWithAttachments(body, attachments),
|
||||
msgSubType,
|
||||
});
|
||||
if (!isActive()) return;
|
||||
attachments.forEach((attachment) => markArweaveAttachmentPlaced(state.session.login, attachment));
|
||||
close();
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
setBusy(false);
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось отправить сообщение.');
|
||||
}
|
||||
@@ -1278,10 +1293,11 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
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, 'Не удалось добавить вложение.');
|
||||
}
|
||||
});
|
||||
@@ -1327,7 +1343,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="edit-message-modal">
|
||||
@@ -1361,16 +1377,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('#edit-message-delete')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await onDelete();
|
||||
if (!isActive()) return;
|
||||
close();
|
||||
} catch (error) {
|
||||
if (!isActive()) return;
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
|
||||
}
|
||||
});
|
||||
@@ -1726,7 +1746,7 @@ function applyPendingScroll(screen, routeKey, forceBottom = false) {
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(doScroll, 20);
|
||||
return window.setTimeout(doScroll, 20);
|
||||
}
|
||||
|
||||
function mapChannelMetaEvent(event, fallbackChannel) {
|
||||
@@ -1782,6 +1802,7 @@ function renderPostCard(post, {
|
||||
onRepost,
|
||||
onShare,
|
||||
onEdit,
|
||||
isActive = () => true,
|
||||
}) {
|
||||
const versionsTotal = Number(post?.versionsTotal || 1);
|
||||
|
||||
@@ -1792,7 +1813,7 @@ function renderPostCard(post, {
|
||||
|
||||
const authorTile = document.createElement('button');
|
||||
authorTile.type = 'button';
|
||||
authorTile.className = 'channel-message-author-tile';
|
||||
authorTile.className = 'ui-button channel-message-author-tile';
|
||||
|
||||
const avatar = createMessageAvatar(post.authorLogin);
|
||||
|
||||
@@ -1821,7 +1842,7 @@ function renderPostCard(post, {
|
||||
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) => {
|
||||
@@ -1841,7 +1862,7 @@ function renderPostCard(post, {
|
||||
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 onStatusAction === 'function') {
|
||||
typeButton.addEventListener('click', (event) => {
|
||||
@@ -1875,7 +1896,7 @@ function renderPostCard(post, {
|
||||
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 = `Удалённое сообщение от ${post.authorLogin}`;
|
||||
deleted.title = 'Открыть историю изменений';
|
||||
deleted.addEventListener('click', (event) => {
|
||||
@@ -1946,7 +1967,7 @@ function renderPostCard(post, {
|
||||
|
||||
const likeButton = document.createElement('button');
|
||||
likeButton.type = 'button';
|
||||
likeButton.className = 'channel-action-item channel-action-like';
|
||||
likeButton.className = 'ui-button channel-action-item channel-action-like';
|
||||
const isLiked = post.reactionState === 'liked';
|
||||
if (isLiked) likeButton.classList.add('is-liked');
|
||||
likeButton.innerHTML = `
|
||||
@@ -1972,7 +1993,7 @@ function renderPostCard(post, {
|
||||
|
||||
const replyButton = document.createElement('button');
|
||||
replyButton.type = 'button';
|
||||
replyButton.className = 'channel-action-item channel-action-reply';
|
||||
replyButton.className = 'ui-button channel-action-item channel-action-reply';
|
||||
replyButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">💬</span>
|
||||
<span class="channel-action-label">Ответить</span>
|
||||
@@ -1985,6 +2006,7 @@ function renderPostCard(post, {
|
||||
openReplyModal({
|
||||
navigate,
|
||||
onSubmit: async (text) => onReply(post.messageRef, text),
|
||||
isActive,
|
||||
});
|
||||
});
|
||||
// Rating/opinion action is intentionally hidden from UI for now.
|
||||
@@ -1994,7 +2016,7 @@ function renderPostCard(post, {
|
||||
|
||||
const shareButton = document.createElement('button');
|
||||
shareButton.type = 'button';
|
||||
shareButton.className = 'channel-action-item channel-action-share';
|
||||
shareButton.className = 'ui-button channel-action-item channel-action-share';
|
||||
shareButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">↗</span>
|
||||
<span class="channel-action-label">Отправить</span>
|
||||
@@ -2011,7 +2033,7 @@ function renderPostCard(post, {
|
||||
if (post.msgSubType === MSG_SUBTYPE_TEXT_REPOST && post.targetRef?.blockchainName && Number.isFinite(post.targetRef?.blockNumber) && post.targetRef?.blockHash) {
|
||||
const originalBtn = document.createElement('button');
|
||||
originalBtn.type = 'button';
|
||||
originalBtn.className = 'channel-action-item';
|
||||
originalBtn.className = 'ui-button channel-action-item';
|
||||
originalBtn.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">↪</span>
|
||||
<span class="channel-action-label">Оригинал</span>
|
||||
@@ -2033,7 +2055,7 @@ function renderPostCard(post, {
|
||||
}
|
||||
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>
|
||||
@@ -2049,13 +2071,13 @@ function renderPostCard(post, {
|
||||
raw: post.rawMessage,
|
||||
localNumber: post.localNumber,
|
||||
msgSubType: post.msgSubType,
|
||||
}));
|
||||
}), { isActive });
|
||||
});
|
||||
actions.append(detailsButton);
|
||||
if (post.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 = `
|
||||
@@ -2155,6 +2177,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
onRepost: handlers.onRepost,
|
||||
onShare: handlers.onShare,
|
||||
onEdit: handlers.onEdit,
|
||||
isActive: handlers.isActive,
|
||||
});
|
||||
const key = messageRefKey(item.post.messageRef);
|
||||
if (key) {
|
||||
@@ -2192,10 +2215,10 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
}
|
||||
|
||||
const hasPendingScrollTarget = pendingScrollByRoute.has(routeKey);
|
||||
applyPendingScroll(screen, routeKey, channelData.isOwnChannel || channelData.isDiary || unreadCount === 0);
|
||||
if (!hasPendingScrollTarget && unreadCount > 0 && !channelData.isOwnChannel && !channelData.isDiary) {
|
||||
window.setTimeout(() => scrollChannelToUnreadLine(screen, unreadCount, false), 40);
|
||||
}
|
||||
const pendingScrollTimer = applyPendingScroll(screen, routeKey, channelData.isOwnChannel || channelData.isDiary || unreadCount === 0);
|
||||
const unreadScrollTimer = !hasPendingScrollTarget && unreadCount > 0 && !channelData.isOwnChannel && !channelData.isDiary
|
||||
? window.setTimeout(() => scrollChannelToUnreadLine(screen, unreadCount, false), 40)
|
||||
: 0;
|
||||
|
||||
const tracker = createChannelReadTracker({
|
||||
screen,
|
||||
@@ -2209,7 +2232,11 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
initialSeenCount: readCount,
|
||||
});
|
||||
|
||||
return tracker.cleanup;
|
||||
return () => {
|
||||
if (pendingScrollTimer) window.clearTimeout(pendingScrollTimer);
|
||||
if (unreadScrollTimer) window.clearTimeout(unreadScrollTimer);
|
||||
tracker.cleanup();
|
||||
};
|
||||
}
|
||||
|
||||
function renderSkeleton(screen) {
|
||||
@@ -2227,14 +2254,22 @@ export function render({ navigate, route, chrome }) {
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack channels-screen channels-screen--channel';
|
||||
const appScreen = document.getElementById('app-screen');
|
||||
appScreen?.classList.add('channels-scroll-clean');
|
||||
|
||||
let disposed = false;
|
||||
let refreshSeq = 0;
|
||||
let refresh = () => {};
|
||||
let cleanupSeenTracking = null;
|
||||
|
||||
const statusBox = document.createElement('div');
|
||||
statusBox.className = 'card status-line is-unavailable channels-status';
|
||||
statusBox.style.display = 'none';
|
||||
|
||||
const ensureActive = () => {
|
||||
if (disposed) throw new Error('Экран канала уже закрыт.');
|
||||
};
|
||||
|
||||
const showStatus = (message) => {
|
||||
if (disposed) return;
|
||||
if (!message) {
|
||||
statusBox.style.display = 'none';
|
||||
statusBox.textContent = '';
|
||||
@@ -2244,36 +2279,79 @@ export function render({ navigate, route, chrome }) {
|
||||
statusBox.style.display = '';
|
||||
};
|
||||
|
||||
let activeChannelData = null;
|
||||
|
||||
const channelHeaderButton = document.createElement('button');
|
||||
channelHeaderButton.type = 'button';
|
||||
channelHeaderButton.className = 'icon-btn channel-header-route-btn app-topbar-title-action';
|
||||
channelHeaderButton.textContent = 'Канал: ...';
|
||||
channelHeaderButton.disabled = true;
|
||||
|
||||
const header = renderHeader({
|
||||
centerNode: channelHeaderButton,
|
||||
leftAction: { label: '<', onClick: () => navigate('channels-list') },
|
||||
rightActions: [
|
||||
const header = createTopBar({
|
||||
center: channelHeaderButton,
|
||||
back: { onClick: () => navigate('channels-list') },
|
||||
className: 'channel-view-topbar',
|
||||
actions: [
|
||||
{ label: 'Оглавление', className: 'channel-header-entrypoint-btn', onClick: () => {} },
|
||||
{ label: '⋯', className: 'channel-header-more-btn', onClick: () => {} },
|
||||
{
|
||||
label: '⋯',
|
||||
title: 'Действия канала',
|
||||
ariaLabel: 'Открыть меню канала',
|
||||
className: 'channel-header-more-btn',
|
||||
menu: {
|
||||
minWidth: 210,
|
||||
items: () => {
|
||||
const apiData = activeChannelData;
|
||||
if (!apiData) return [];
|
||||
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 ?? '',
|
||||
});
|
||||
const items = [
|
||||
{ label: 'О канале', action: () => { if (aboutRoute) navigate(aboutRoute); } },
|
||||
];
|
||||
if (apiData?.isSubscribed && !apiData?.isOwnChannel) {
|
||||
items.push({
|
||||
label: 'Отписаться от канала',
|
||||
danger: true,
|
||||
action: async () => {
|
||||
try {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockFollowChannel({
|
||||
login,
|
||||
storagePwd,
|
||||
targetBlockchainName: apiData.selector.ownerBlockchainName,
|
||||
targetBlockNumber: apiData.selector.channelRootBlockNumber,
|
||||
targetBlockHashHex: apiData.selector.channelRootBlockHash,
|
||||
unfollow: true,
|
||||
});
|
||||
if (disposed) return;
|
||||
const feed = await authService.listSubscriptionsFeed(login, 200);
|
||||
if (disposed) return;
|
||||
setChannelsFeed(feed, state.channelsIndex);
|
||||
showToast('Вы отписались от канала');
|
||||
void refresh();
|
||||
} catch (error) {
|
||||
showStatus(toUserMessage(error, 'Не удалось отписаться от канала.'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
header.classList.add('channel-view-topbar');
|
||||
const channelEntrypointButton = header.querySelector('.header-actions .channel-header-entrypoint-btn');
|
||||
const channelMoreButton = header.querySelector('.header-actions .channel-header-more-btn');
|
||||
const channelEntrypointButton = header.querySelector('.topbar__right .channel-header-entrypoint-btn');
|
||||
const channelMoreButton = header.querySelector('.topbar__right .channel-header-more-btn');
|
||||
if (channelEntrypointButton) {
|
||||
channelEntrypointButton.disabled = true;
|
||||
channelEntrypointButton.hidden = true;
|
||||
}
|
||||
chrome?.setTopbar(header);
|
||||
|
||||
const rerender = () => {
|
||||
const current = document.querySelector('section.channels-screen--channel');
|
||||
if (!current) return;
|
||||
const next = render({ navigate, route });
|
||||
current.cleanup?.();
|
||||
current.replaceWith(next);
|
||||
};
|
||||
let activeSelector = null;
|
||||
|
||||
const requireSigningSession = () => {
|
||||
@@ -2305,12 +2383,14 @@ export function render({ navigate, route, chrome }) {
|
||||
} else {
|
||||
await authService.addBlockLike({ login, storagePwd, message: messageRef });
|
||||
}
|
||||
if (disposed) return;
|
||||
setMessageReactionState(messageRef, nextReaction);
|
||||
softHaptic(10);
|
||||
rerender();
|
||||
void refresh();
|
||||
} catch (error) {
|
||||
if (disposed) return;
|
||||
setMessageReactionState(messageRef, previousReaction || 'unliked');
|
||||
rerender();
|
||||
void refresh();
|
||||
throw error;
|
||||
} finally {
|
||||
pendingReactionActions.delete(actionKey);
|
||||
@@ -2320,25 +2400,27 @@ export function render({ navigate, route, chrome }) {
|
||||
const onReply = async (messageRef, text) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockReply({ login, storagePwd, message: messageRef, text });
|
||||
ensureActive();
|
||||
|
||||
const scrollTarget = messageRefKey(messageRef);
|
||||
if (scrollTarget) pendingScrollByRoute.set(routeKey, scrollTarget);
|
||||
|
||||
softHaptic(15);
|
||||
showToast('Ответ отправлен');
|
||||
rerender();
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const onRating = async (messageRef, text) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockRating({ login, storagePwd, message: messageRef, text });
|
||||
ensureActive();
|
||||
|
||||
const scrollTarget = messageRefKey(messageRef);
|
||||
if (scrollTarget) pendingScrollByRoute.set(routeKey, scrollTarget);
|
||||
|
||||
softHaptic(15);
|
||||
showToast('Оценка отправлена');
|
||||
rerender();
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const onStatusAction = async (post) => {
|
||||
@@ -2348,10 +2430,12 @@ export function render({ navigate, route, chrome }) {
|
||||
openStatusActionMenuModal({
|
||||
targetLabel: typeMeta.label,
|
||||
options,
|
||||
isActive: () => !disposed,
|
||||
onSelect: async (option) => {
|
||||
openStatusActionCommentModal({
|
||||
title: option.modalTitle,
|
||||
submitLabel: option.label,
|
||||
isActive: () => !disposed,
|
||||
onSubmit: async (text) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockStatusAction({
|
||||
@@ -2361,9 +2445,10 @@ export function render({ navigate, route, chrome }) {
|
||||
text,
|
||||
statusSubType: option.subType,
|
||||
});
|
||||
ensureActive();
|
||||
softHaptic(14);
|
||||
showToast(`${option.label} сохранено`);
|
||||
rerender();
|
||||
void refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -2403,10 +2488,12 @@ export function render({ navigate, route, chrome }) {
|
||||
const onRepost = async (messageRef) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
const channels = await loadOwnedChannelsForRepost(login);
|
||||
if (disposed) return;
|
||||
if (!channels.length) throw new Error('У вас пока нет каналов для репоста.');
|
||||
openRepostModal({
|
||||
navigate,
|
||||
channels,
|
||||
isActive: () => !disposed,
|
||||
onSubmit: async ({ channel, text }) => {
|
||||
await authService.addBlockRepost({
|
||||
login,
|
||||
@@ -2415,9 +2502,10 @@ export function render({ navigate, route, chrome }) {
|
||||
message: messageRef,
|
||||
text,
|
||||
});
|
||||
ensureActive();
|
||||
if (isSameChannelSelector(channel, activeSelector)) {
|
||||
pendingScrollByRoute.set(routeKey, '__LAST__');
|
||||
rerender();
|
||||
void refresh();
|
||||
}
|
||||
softHaptic(12);
|
||||
showToast('Репост опубликован');
|
||||
@@ -2434,6 +2522,7 @@ export function render({ navigate, route, chrome }) {
|
||||
text: 'Тред из канала SHiNE',
|
||||
url: buildAbsoluteRouteUrl(routeToShare),
|
||||
});
|
||||
if (disposed) return;
|
||||
if (result === 'copied') showToast('Ссылка скопирована');
|
||||
if (result === 'shared') showToast('Ссылка передана');
|
||||
if (result === 'shared' || result === 'copied') softHaptic(10);
|
||||
@@ -2455,11 +2544,12 @@ export function render({ navigate, route, chrome }) {
|
||||
text: bodyText,
|
||||
msgSubType,
|
||||
});
|
||||
ensureActive();
|
||||
|
||||
pendingScrollByRoute.set(routeKey, '__LAST__');
|
||||
softHaptic(15);
|
||||
showToast('Сообщение отправлено');
|
||||
rerender();
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const onEditPost = async (messageRef, text) => {
|
||||
@@ -2476,9 +2566,10 @@ export function render({ navigate, route, chrome }) {
|
||||
isChannelPost: !isDiaryEdit,
|
||||
channel: isDiaryEdit ? null : activeSelector,
|
||||
});
|
||||
ensureActive();
|
||||
softHaptic(12);
|
||||
showToast('Сообщение обновлено');
|
||||
rerender();
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const onEditChannelMeta = async ({ title, description, avatar }) => {
|
||||
@@ -2494,21 +2585,65 @@ export function render({ navigate, route, chrome }) {
|
||||
description,
|
||||
avatar,
|
||||
});
|
||||
ensureActive();
|
||||
if (avatar?.ar) markArweaveAttachmentPlaced(login, avatar);
|
||||
softHaptic(12);
|
||||
showToast('Профиль канала обновлён');
|
||||
rerender();
|
||||
void refresh();
|
||||
};
|
||||
|
||||
screen.append(statusBox);
|
||||
|
||||
const skeleton = renderSkeleton(screen);
|
||||
const clearContent = () => {
|
||||
if (typeof cleanupSeenTracking === 'function') cleanupSeenTracking();
|
||||
cleanupSeenTracking = null;
|
||||
Array.from(screen.children).forEach((child) => {
|
||||
if (child !== statusBox) child.remove();
|
||||
});
|
||||
};
|
||||
|
||||
let cleanupSeenTracking = null;
|
||||
const clearOwnedModal = () => {
|
||||
const modalRoot = document.getElementById('modal-root');
|
||||
if (!modalRoot) return;
|
||||
const ownedSelector = [
|
||||
'#about-channel-modal',
|
||||
'#blockchain-details-modal',
|
||||
'#channel-entrypoint-history-modal',
|
||||
'#channel-entrypoint-menu-modal',
|
||||
'#channel-message-modal',
|
||||
'#channel-status-action-modal',
|
||||
'#channel-status-menu-modal',
|
||||
'#edit-channel-modal',
|
||||
'#edit-message-modal',
|
||||
'#message-history-modal',
|
||||
'#reply-modal',
|
||||
'#repost-modal',
|
||||
].join(',');
|
||||
if (modalRoot.querySelector(ownedSelector)) modalRoot.innerHTML = '';
|
||||
};
|
||||
|
||||
refresh = async () => {
|
||||
if (disposed) return;
|
||||
const seq = ++refreshSeq;
|
||||
clearContent();
|
||||
activeChannelData = null;
|
||||
activeSelector = null;
|
||||
showStatus('');
|
||||
channelHeaderButton.textContent = 'Канал: ...';
|
||||
channelHeaderButton.disabled = true;
|
||||
channelHeaderButton.onclick = null;
|
||||
if (channelMoreButton) channelMoreButton.disabled = true;
|
||||
if (channelEntrypointButton) {
|
||||
channelEntrypointButton.disabled = true;
|
||||
channelEntrypointButton.hidden = true;
|
||||
channelEntrypointButton.onclick = null;
|
||||
}
|
||||
|
||||
const skeleton = renderSkeleton(screen);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const apiData = await loadFromApi(route, channelId);
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
activeSelector = apiData?.selector || null;
|
||||
const titleLabel = apiData?.channel?.displayTitle || apiData?.channel?.name || 'channel';
|
||||
const entrypointPosts = getEntrypointPosts(apiData?.posts);
|
||||
@@ -2536,35 +2671,9 @@ export function render({ navigate, route, chrome }) {
|
||||
if (aboutRoute) navigate(aboutRoute);
|
||||
};
|
||||
}
|
||||
activeChannelData = apiData;
|
||||
if (channelMoreButton) {
|
||||
channelMoreButton.disabled = false;
|
||||
channelMoreButton.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
header.querySelector('.channel-header-more-menu')?.remove();
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'channel-header-more-menu';
|
||||
const about = document.createElement('button'); about.type='button'; about.textContent='О канале';
|
||||
about.onclick = () => {
|
||||
const aboutRoute = makeShineChannelAboutRoute({ ownerBlockchainName: apiData?.channel?.ownerBlockchainName || apiData?.selector?.ownerBlockchainName || '', channelRootBlockNumber: apiData?.selector?.channelRootBlockNumber ?? '', channelRootBlockHash: apiData?.selector?.channelRootBlockHash ?? '' });
|
||||
menu.remove(); if (aboutRoute) navigate(aboutRoute);
|
||||
};
|
||||
menu.append(about);
|
||||
if (apiData?.isSubscribed && !apiData?.isOwnChannel) {
|
||||
const unfollow = document.createElement('button'); unfollow.type='button'; unfollow.className='is-danger'; unfollow.textContent='Отписаться от канала';
|
||||
unfollow.onclick = async () => {
|
||||
menu.remove();
|
||||
try {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockFollowChannel({ login, storagePwd, targetBlockchainName: apiData.selector.ownerBlockchainName, targetBlockNumber: apiData.selector.channelRootBlockNumber, targetBlockHashHex: apiData.selector.channelRootBlockHash, unfollow: true });
|
||||
const feed = await authService.listSubscriptionsFeed(login, 200); setChannelsFeed(feed, state.channelsIndex); showToast('Вы отписались от канала'); rerender();
|
||||
} catch (error) { showStatus(toUserMessage(error, 'Не удалось отписаться от канала.')); }
|
||||
};
|
||||
menu.append(unfollow);
|
||||
}
|
||||
header.append(menu);
|
||||
const close = (e) => { if (!menu.contains(e.target) && e.target !== channelMoreButton) { menu.remove(); document.removeEventListener('click', close, true); } };
|
||||
setTimeout(() => document.addEventListener('click', close, true), 0);
|
||||
};
|
||||
}
|
||||
if (channelEntrypointButton) {
|
||||
const canShowEntrypointButton = !apiData?.isDiary && entrypointPosts.length > 0;
|
||||
@@ -2579,10 +2688,12 @@ export function render({ navigate, route, chrome }) {
|
||||
}
|
||||
skeleton.remove();
|
||||
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
|
||||
isActive: () => !disposed,
|
||||
onAddMessage: () => {
|
||||
openAddMessageModal({
|
||||
channelName: apiData?.channel?.name || '',
|
||||
navigate,
|
||||
isActive: () => !disposed,
|
||||
onSubmit: async ({ text: bodyText, msgSubType }) => {
|
||||
try {
|
||||
await onAddPost(bodyText, msgSubType);
|
||||
@@ -2672,31 +2783,41 @@ export function render({ navigate, route, chrome }) {
|
||||
targetBlockHashHex: apiData.selector.channelRootBlockHash,
|
||||
unfollow: false,
|
||||
});
|
||||
if (disposed) return;
|
||||
|
||||
const feed = await authService.listSubscriptionsFeed(login, 200);
|
||||
if (disposed) return;
|
||||
setChannelsFeed(feed, state.channelsIndex);
|
||||
softHaptic(15);
|
||||
showToast('Подписка на канал выполнена');
|
||||
rerender();
|
||||
void refresh();
|
||||
} catch (error) {
|
||||
showStatus(toUserMessage(error, 'Не удалось подписаться на канал.'));
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (disposed || seq !== refreshSeq) return;
|
||||
skeleton.remove();
|
||||
if (isChannelsDemoMode()) {
|
||||
renderDemoFallback(screen, navigate, error);
|
||||
return;
|
||||
}
|
||||
renderLoadError(screen, navigate, toUserMessage(error, 'Не удалось загрузить канал.'), rerender);
|
||||
renderLoadError(screen, navigate, toUserMessage(error, 'Не удалось загрузить канал.'), () => {
|
||||
void refresh();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
screen.cleanup = () => {
|
||||
appScreen?.classList.remove('channels-scroll-clean');
|
||||
if (typeof cleanupSeenTracking === 'function') cleanupSeenTracking();
|
||||
};
|
||||
|
||||
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