SHA256
Channels UI + read/unread + unique views + style polish
This commit is contained in:
@@ -9,6 +9,9 @@ import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
import {
|
||||
animatePress,
|
||||
createSkeletonCard,
|
||||
formatRelativeTime,
|
||||
longPressFeel,
|
||||
shareOrCopyLink,
|
||||
showToast,
|
||||
softHaptic,
|
||||
} from '../services/channels-ux.js';
|
||||
@@ -17,6 +20,10 @@ export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
||||
|
||||
const pendingReactionActions = new Set();
|
||||
const pendingScrollByRoute = new Map();
|
||||
const revealedCountersByRoute = new Map();
|
||||
const seenFlushTimersByRoute = new Map();
|
||||
const seenPendingByRoute = new Map();
|
||||
const firstUnreadJumpByRoute = new Map();
|
||||
|
||||
function isChannelsDemoMode() {
|
||||
try {
|
||||
@@ -66,6 +73,59 @@ function messageRefKey(messageRef) {
|
||||
return `${blockchainName}:${blockNumber}:${blockHash}`;
|
||||
}
|
||||
|
||||
function parseMessageRefKey(key) {
|
||||
const raw = String(key || '').trim();
|
||||
if (!raw) return null;
|
||||
const parts = raw.split(':');
|
||||
if (parts.length !== 3) return null;
|
||||
const blockNumber = Number(parts[1]);
|
||||
const blockHash = normalizeMessageHash(parts[2]);
|
||||
if (!parts[0] || !Number.isFinite(blockNumber) || blockNumber < 0 || !blockHash) return null;
|
||||
return {
|
||||
blockchainName: parts[0],
|
||||
blockNumber,
|
||||
blockHash,
|
||||
};
|
||||
}
|
||||
|
||||
function blockRefToMessageKey(blockRef, fallbackBch = '') {
|
||||
const blockNumber = toSafeInt(blockRef?.blockNumber);
|
||||
const blockHash = normalizeMessageHash(blockRef?.blockHash);
|
||||
const blockchainName = String(fallbackBch || '').trim();
|
||||
if (!blockchainName || blockNumber == null || !blockHash) return '';
|
||||
return `${blockchainName}:${blockNumber}:${blockHash}`;
|
||||
}
|
||||
|
||||
function getRevealedCounterSet(routeKey) {
|
||||
const key = String(routeKey || '').trim();
|
||||
if (!key) return new Set();
|
||||
let bucket = revealedCountersByRoute.get(key);
|
||||
if (!bucket) {
|
||||
bucket = new Set();
|
||||
revealedCountersByRoute.set(key, bucket);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function isCounterVisible(routeKey, counterKey) {
|
||||
const key = String(counterKey || '').trim();
|
||||
if (!key) return false;
|
||||
return getRevealedCounterSet(routeKey).has(key);
|
||||
}
|
||||
|
||||
function revealCounter(routeKey, counterKey) {
|
||||
const key = String(counterKey || '').trim();
|
||||
if (!key) return;
|
||||
getRevealedCounterSet(routeKey).add(key);
|
||||
}
|
||||
|
||||
function buildAbsoluteRouteUrl(routePath = '') {
|
||||
const cleanRoute = String(routePath || '').replace(/^#?\/?/, '');
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = `#/${cleanRoute}`;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function channelDescriptionParamKey(selector) {
|
||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||
const rootNo = Number(selector?.channelRootBlockNumber);
|
||||
@@ -165,6 +225,43 @@ function resolveMessageText(message) {
|
||||
);
|
||||
}
|
||||
|
||||
function toTimestampMs(...candidates) {
|
||||
for (const candidate of candidates) {
|
||||
if (candidate == null) continue;
|
||||
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
|
||||
return candidate > 1e12 ? Math.round(candidate) : Math.round(candidate * 1000);
|
||||
}
|
||||
if (typeof candidate === 'string') {
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) continue;
|
||||
const asNum = Number(trimmed);
|
||||
if (Number.isFinite(asNum) && asNum > 0) {
|
||||
return asNum > 1e12 ? Math.round(asNum) : Math.round(asNum * 1000);
|
||||
}
|
||||
const parsed = Date.parse(trimmed);
|
||||
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function resolveMessageTimestampMs(message) {
|
||||
return toTimestampMs(
|
||||
message?.messageTimeMs,
|
||||
message?.message_time_ms,
|
||||
message?.timeMs,
|
||||
message?.time_ms,
|
||||
message?.timestampMs,
|
||||
message?.timestamp_ms,
|
||||
message?.createdAtMs,
|
||||
message?.created_at_ms,
|
||||
message?.messageTime,
|
||||
message?.createdAt,
|
||||
message?.created_at,
|
||||
message?.timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
function openAboutChannelModal(channel) {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
@@ -405,6 +502,9 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
body: resolvedText || '(пусто)',
|
||||
likesCount: Number(message?.likesCount || 0),
|
||||
repliesCount: Number(message?.repliesCount || 0),
|
||||
viewCount: Number(message?.viewCount || 0),
|
||||
seenByMe: message?.seenByMe === true,
|
||||
timestampMs: resolveMessageTimestampMs(message),
|
||||
messageRef,
|
||||
reactionState: messageRef ? getMessageReactionState(messageRef) : '',
|
||||
};
|
||||
@@ -420,6 +520,8 @@ async function loadFromApi(route, channelId) {
|
||||
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
||||
const posts = messages.map((message, index) => mapApiMessageToPost(message, selector, index + 1));
|
||||
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
|
||||
const firstUnreadKey = blockRefToMessageKey(payload.firstUnreadMessageRef, selector.ownerBlockchainName);
|
||||
const unreadFromPayload = Number(payload.unreadCount || 0);
|
||||
|
||||
const readDescription = async () => {
|
||||
const sourceDescription = String(payload.channel?.channelDescription || '').trim();
|
||||
@@ -445,6 +547,10 @@ async function loadFromApi(route, channelId) {
|
||||
ownerName: ownerLogin || 'неизвестно',
|
||||
},
|
||||
posts,
|
||||
unreadCount: Number.isFinite(unreadFromPayload)
|
||||
? Math.max(0, unreadFromPayload)
|
||||
: posts.filter((post) => post.seenByMe !== true).length,
|
||||
firstUnreadKey,
|
||||
isOwnChannel: ownerLogin.toLowerCase() === (state.session.login || '').toLowerCase(),
|
||||
selector,
|
||||
};
|
||||
@@ -516,13 +622,29 @@ function applyPendingScroll(screen, routeKey) {
|
||||
setTimeout(doScroll, 20);
|
||||
}
|
||||
|
||||
function renderPostCard(post, { navigate, selector, onToggleLike, onReply }) {
|
||||
function renderPostCard(post, {
|
||||
navigate,
|
||||
routeKey,
|
||||
selector,
|
||||
onToggleLike,
|
||||
onReply,
|
||||
onShare,
|
||||
}) {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card stack channel-message-card';
|
||||
|
||||
const topRow = document.createElement('div');
|
||||
topRow.className = 'channel-message-top';
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'channel-message-avatar';
|
||||
avatar.textContent = String(post.authorLogin || 'A').trim().charAt(0).toUpperCase() || 'A';
|
||||
|
||||
const authorBlock = document.createElement('div');
|
||||
authorBlock.className = 'channel-message-author';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'channel-message-title author-line';
|
||||
|
||||
const loginEl = document.createElement('span');
|
||||
loginEl.className = 'author-line-login';
|
||||
loginEl.textContent = post.authorLogin;
|
||||
@@ -531,22 +653,41 @@ function renderPostCard(post, { navigate, selector, onToggleLike, onReply }) {
|
||||
numberEl.className = 'author-line-num';
|
||||
numberEl.textContent = `· #${post.localNumber}`;
|
||||
|
||||
const timestamp = document.createElement('div');
|
||||
timestamp.className = 'channel-message-time';
|
||||
timestamp.textContent = post.timestampMs ? formatRelativeTime(post.timestampMs) : '—';
|
||||
|
||||
title.append(loginEl, numberEl);
|
||||
authorBlock.append(title, timestamp);
|
||||
topRow.append(avatar, authorBlock);
|
||||
|
||||
const body = document.createElement('p');
|
||||
body.className = 'channel-message-body';
|
||||
body.textContent = post.body;
|
||||
|
||||
const stats = document.createElement('p');
|
||||
stats.className = 'channel-message-stats';
|
||||
stats.textContent = `Лайки: ${post.likesCount || 0}, ответы: ${post.repliesCount || 0}`;
|
||||
const views = document.createElement('p');
|
||||
views.className = 'channel-message-views';
|
||||
views.textContent = `Просмотры: ${Number(post.viewCount || 0)}`;
|
||||
|
||||
card.append(title, body, stats);
|
||||
card.append(topRow, body, views);
|
||||
|
||||
const refKey = messageRefKey(post.messageRef);
|
||||
if (refKey) {
|
||||
card.dataset.messageKey = refKey;
|
||||
}
|
||||
const countersVisible = refKey ? isCounterVisible(routeKey, refKey) : true;
|
||||
if (!countersVisible) {
|
||||
card.classList.remove('is-counters-visible');
|
||||
} else {
|
||||
card.classList.add('is-counters-visible');
|
||||
}
|
||||
|
||||
const revealCounters = () => {
|
||||
if (!refKey) return;
|
||||
revealCounter(routeKey, refKey);
|
||||
card.classList.add('is-counters-visible');
|
||||
};
|
||||
card.addEventListener('click', revealCounters);
|
||||
|
||||
if (!post.messageRef || !selector) return card;
|
||||
|
||||
@@ -558,25 +699,36 @@ function renderPostCard(post, { navigate, selector, onToggleLike, onReply }) {
|
||||
|
||||
const likeButton = document.createElement('button');
|
||||
likeButton.type = 'button';
|
||||
likeButton.className = 'secondary-btn channel-action-like';
|
||||
likeButton.className = 'channel-action-item channel-action-like';
|
||||
const isLiked = post.reactionState === 'liked';
|
||||
if (isLiked) likeButton.classList.add('is-liked');
|
||||
likeButton.textContent = isPending ? 'Выполняется...' : (isLiked ? 'Убрать лайк' : 'Лайк');
|
||||
likeButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">✦</span>
|
||||
<span class="channel-action-label">${isPending ? 'Сияние...' : 'Сияние'}</span>
|
||||
<span class="channel-action-counter">${post.likesCount || 0}</span>
|
||||
`;
|
||||
likeButton.disabled = isPending;
|
||||
likeButton.addEventListener('click', async (event) => {
|
||||
animatePress(event.currentTarget);
|
||||
if (isPending) return;
|
||||
revealCounters();
|
||||
await longPressFeel(event.currentTarget, 130);
|
||||
likeButton.disabled = true;
|
||||
likeButton.textContent = 'Выполняется...';
|
||||
const labelEl = likeButton.querySelector('.channel-action-label');
|
||||
if (labelEl) labelEl.textContent = 'Сияние...';
|
||||
await onToggleLike(post.messageRef, isLiked ? 'unlike' : 'like', { likeButton });
|
||||
});
|
||||
|
||||
const replyButton = document.createElement('button');
|
||||
replyButton.type = 'button';
|
||||
replyButton.className = 'secondary-btn channel-action-reply';
|
||||
replyButton.textContent = 'Ответить';
|
||||
replyButton.className = 'channel-action-item channel-action-reply';
|
||||
replyButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">⟳</span>
|
||||
<span class="channel-action-label">Отразить</span>
|
||||
`;
|
||||
replyButton.addEventListener('click', (event) => {
|
||||
animatePress(event.currentTarget);
|
||||
revealCounters();
|
||||
openReplyModal({
|
||||
onSubmit: async (text) => onReply(post.messageRef, text),
|
||||
});
|
||||
@@ -584,15 +736,35 @@ function renderPostCard(post, { navigate, selector, onToggleLike, onReply }) {
|
||||
|
||||
const openThreadButton = document.createElement('button');
|
||||
openThreadButton.type = 'button';
|
||||
openThreadButton.className = 'secondary-btn channel-action-thread';
|
||||
openThreadButton.textContent = 'Открыть тред';
|
||||
openThreadButton.className = 'channel-action-item channel-action-thread';
|
||||
openThreadButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">#</span>
|
||||
<span class="channel-action-label">Тред</span>
|
||||
<span class="channel-action-counter">${post.repliesCount || 0}</span>
|
||||
`;
|
||||
openThreadButton.addEventListener('click', (event) => {
|
||||
animatePress(event.currentTarget);
|
||||
revealCounters();
|
||||
const route = buildThreadRoute(post.messageRef, selector);
|
||||
if (route) navigate(route);
|
||||
});
|
||||
|
||||
actions.append(likeButton, replyButton, openThreadButton);
|
||||
const shareButton = document.createElement('button');
|
||||
shareButton.type = 'button';
|
||||
shareButton.className = 'channel-action-item channel-action-share';
|
||||
shareButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">↗</span>
|
||||
<span class="channel-action-label">Транслировать</span>
|
||||
`;
|
||||
shareButton.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
animatePress(event.currentTarget);
|
||||
revealCounters();
|
||||
const route = buildThreadRoute(post.messageRef, selector);
|
||||
await onShare(route);
|
||||
});
|
||||
|
||||
actions.append(likeButton, replyButton, openThreadButton, shareButton);
|
||||
card.append(actions);
|
||||
return card;
|
||||
}
|
||||
@@ -648,15 +820,38 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
|
||||
const feed = document.createElement('div');
|
||||
feed.className = 'stack channel-feed';
|
||||
const unreadDivider = document.createElement('div');
|
||||
unreadDivider.className = 'channels-unread-divider';
|
||||
unreadDivider.textContent = 'Непрочитанные сообщения';
|
||||
const unreadJump = document.createElement('button');
|
||||
unreadJump.type = 'button';
|
||||
unreadJump.className = 'channels-unread-jump';
|
||||
unreadJump.innerHTML = `
|
||||
<span class="channels-unread-jump-icon" aria-hidden="true">↓</span>
|
||||
<span class="channels-unread-jump-badge"></span>
|
||||
`;
|
||||
const unreadBadge = unreadJump.querySelector('.channels-unread-jump-badge');
|
||||
const postsByKey = new Map();
|
||||
const unreadKeys = new Set();
|
||||
let seenFlushInFlight = false;
|
||||
let seenObserver = null;
|
||||
|
||||
if (channelData.posts.length) {
|
||||
channelData.posts.forEach((post) => {
|
||||
feed.append(renderPostCard(post, {
|
||||
const row = renderPostCard(post, {
|
||||
navigate,
|
||||
routeKey,
|
||||
selector: channelData.selector,
|
||||
onToggleLike: handlers.onToggleLike,
|
||||
onReply: handlers.onReply,
|
||||
}));
|
||||
onShare: handlers.onShare,
|
||||
});
|
||||
const key = messageRefKey(post.messageRef);
|
||||
if (key) {
|
||||
postsByKey.set(key, post);
|
||||
if (post.seenByMe !== true) unreadKeys.add(key);
|
||||
}
|
||||
feed.append(row);
|
||||
});
|
||||
} else {
|
||||
const empty = document.createElement('div');
|
||||
@@ -665,6 +860,102 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
feed.append(empty);
|
||||
}
|
||||
|
||||
const syncUnreadState = () => {
|
||||
unreadKeys.clear();
|
||||
postsByKey.forEach((post, key) => {
|
||||
if (post.seenByMe !== true) unreadKeys.add(key);
|
||||
});
|
||||
};
|
||||
|
||||
const updateUnreadJump = () => {
|
||||
const unreadCount = unreadKeys.size;
|
||||
unreadJump.classList.toggle('is-visible', unreadCount > 0);
|
||||
unreadJump.hidden = unreadCount <= 0;
|
||||
if (unreadBadge) unreadBadge.textContent = unreadCount > 0 ? String(unreadCount) : '';
|
||||
};
|
||||
|
||||
const mountUnreadDivider = () => {
|
||||
unreadDivider.remove();
|
||||
if (!unreadKeys.size) return;
|
||||
const firstUnread = channelData.posts.find((post) => {
|
||||
const key = messageRefKey(post.messageRef);
|
||||
return key && unreadKeys.has(key);
|
||||
});
|
||||
const firstUnreadKey = messageRefKey(firstUnread?.messageRef);
|
||||
if (!firstUnreadKey) return;
|
||||
const target = feed.querySelector(`[data-message-key="${firstUnreadKey}"]`);
|
||||
if (target) {
|
||||
feed.insertBefore(unreadDivider, target);
|
||||
}
|
||||
};
|
||||
|
||||
const routePending = (() => {
|
||||
let bucket = seenPendingByRoute.get(routeKey);
|
||||
if (!bucket) {
|
||||
bucket = new Set();
|
||||
seenPendingByRoute.set(routeKey, bucket);
|
||||
}
|
||||
return bucket;
|
||||
})();
|
||||
|
||||
const scheduleSeenFlush = () => {
|
||||
const oldTimer = seenFlushTimersByRoute.get(routeKey);
|
||||
if (oldTimer) clearTimeout(oldTimer);
|
||||
const timer = setTimeout(async () => {
|
||||
seenFlushTimersByRoute.delete(routeKey);
|
||||
if (seenFlushInFlight) return;
|
||||
|
||||
const pendingKeys = [...routePending].filter((key) => {
|
||||
const post = postsByKey.get(key);
|
||||
return !!post && post.seenByMe !== true;
|
||||
});
|
||||
if (!pendingKeys.length) return;
|
||||
|
||||
const refs = pendingKeys
|
||||
.map((key) => parseMessageRefKey(key))
|
||||
.filter(Boolean);
|
||||
if (!refs.length) return;
|
||||
|
||||
pendingKeys.forEach((key) => routePending.delete(key));
|
||||
seenFlushInFlight = true;
|
||||
try {
|
||||
await handlers.onMarkSeenBatch(refs);
|
||||
refs.forEach((ref) => {
|
||||
const key = messageRefKey(ref);
|
||||
const post = key ? postsByKey.get(key) : null;
|
||||
if (post) post.seenByMe = true;
|
||||
});
|
||||
syncUnreadState();
|
||||
mountUnreadDivider();
|
||||
updateUnreadJump();
|
||||
} catch (error) {
|
||||
refs.forEach((ref) => {
|
||||
const key = messageRefKey(ref);
|
||||
if (!key) return;
|
||||
const node = feed.querySelector(`[data-message-key="${key}"]`);
|
||||
if (node) seenObserver?.observe(node);
|
||||
});
|
||||
handlers.onSeenError?.(error);
|
||||
} finally {
|
||||
seenFlushInFlight = false;
|
||||
if (routePending.size) scheduleSeenFlush();
|
||||
}
|
||||
}, 220);
|
||||
seenFlushTimersByRoute.set(routeKey, timer);
|
||||
};
|
||||
|
||||
unreadJump.addEventListener('click', () => {
|
||||
const unreadPosts = channelData.posts.filter((post) => {
|
||||
const key = messageRefKey(post.messageRef);
|
||||
return key && unreadKeys.has(key);
|
||||
});
|
||||
const targetPost = unreadPosts.length ? unreadPosts[unreadPosts.length - 1] : channelData.posts[channelData.posts.length - 1];
|
||||
const key = messageRefKey(targetPost?.messageRef);
|
||||
if (!key) return;
|
||||
const target = feed.querySelector(`[data-message-key="${key}"]`);
|
||||
target?.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
});
|
||||
|
||||
if (channelData.isOwnChannel) {
|
||||
actionButton.addEventListener('click', (event) => {
|
||||
animatePress(event.currentTarget);
|
||||
@@ -682,8 +973,58 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
backButton.textContent = 'Назад к каналам';
|
||||
backButton.addEventListener('click', () => navigate('channels-list'));
|
||||
|
||||
screen.append(head, actionButton, feed, backButton);
|
||||
screen.append(head, actionButton, feed, backButton, unreadJump);
|
||||
|
||||
if (state.session.login && channelData.selector && channelData.posts.length) {
|
||||
seenObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting || entry.intersectionRatio < 0.6) return;
|
||||
const key = String(entry.target?.dataset?.messageKey || '').trim();
|
||||
if (!key) return;
|
||||
const post = postsByKey.get(key);
|
||||
if (!post || post.seenByMe === true) return;
|
||||
routePending.add(key);
|
||||
seenObserver?.unobserve(entry.target);
|
||||
});
|
||||
if (routePending.size) scheduleSeenFlush();
|
||||
}, {
|
||||
root: document.getElementById('app-screen') || null,
|
||||
threshold: [0.6],
|
||||
});
|
||||
|
||||
feed.querySelectorAll('[data-message-key]').forEach((node) => {
|
||||
const key = String(node.dataset.messageKey || '').trim();
|
||||
if (key && unreadKeys.has(key)) seenObserver?.observe(node);
|
||||
});
|
||||
}
|
||||
|
||||
syncUnreadState();
|
||||
mountUnreadDivider();
|
||||
updateUnreadJump();
|
||||
|
||||
const firstUnreadCandidate = channelData.firstUnreadKey
|
||||
|| (() => {
|
||||
const first = channelData.posts.find((post) => post.seenByMe !== true);
|
||||
return messageRefKey(first?.messageRef);
|
||||
})();
|
||||
if (firstUnreadCandidate) {
|
||||
const previous = firstUnreadJumpByRoute.get(routeKey);
|
||||
if (previous !== firstUnreadCandidate) {
|
||||
pendingScrollByRoute.set(routeKey, firstUnreadCandidate);
|
||||
firstUnreadJumpByRoute.set(routeKey, firstUnreadCandidate);
|
||||
}
|
||||
} else {
|
||||
firstUnreadJumpByRoute.delete(routeKey);
|
||||
}
|
||||
|
||||
applyPendingScroll(screen, routeKey);
|
||||
return () => {
|
||||
seenObserver?.disconnect();
|
||||
const timer = seenFlushTimersByRoute.get(routeKey);
|
||||
if (timer) clearTimeout(timer);
|
||||
seenFlushTimersByRoute.delete(routeKey);
|
||||
seenPendingByRoute.delete(routeKey);
|
||||
};
|
||||
}
|
||||
|
||||
function renderSkeleton(screen) {
|
||||
@@ -776,6 +1117,38 @@ export function render({ navigate, route }) {
|
||||
rerender();
|
||||
};
|
||||
|
||||
const onMarkSeenBatch = async (refs) => {
|
||||
if (!Array.isArray(refs) || !refs.length) return;
|
||||
const login = String(state.session.login || '').trim();
|
||||
if (!login || !routeSelector?.ownerBlockchainName || routeSelector.channelRootBlockNumber == null) return;
|
||||
await authService.markChannelMessagesSeen({
|
||||
login,
|
||||
channel: {
|
||||
ownerBlockchainName: routeSelector.ownerBlockchainName,
|
||||
channelRootBlockNumber: routeSelector.channelRootBlockNumber,
|
||||
channelRootBlockHash: routeSelector.channelRootBlockHash,
|
||||
},
|
||||
messages: refs,
|
||||
});
|
||||
};
|
||||
|
||||
const onShare = async (routePath) => {
|
||||
try {
|
||||
const routeToShare = String(routePath || '').trim();
|
||||
if (!routeToShare) throw new Error('Не удалось подготовить ссылку на сообщение.');
|
||||
const result = await shareOrCopyLink({
|
||||
title: 'SHiNE · Каналы',
|
||||
text: 'Тред из канала SHiNE',
|
||||
url: buildAbsoluteRouteUrl(routeToShare),
|
||||
});
|
||||
if (result === 'copied') showToast('Ссылка скопирована');
|
||||
if (result === 'shared') showToast('Ссылка передана');
|
||||
if (result === 'shared' || result === 'copied') softHaptic(10);
|
||||
} catch (error) {
|
||||
showStatus(toUserMessage(error, 'Не удалось транслировать ссылку.'));
|
||||
}
|
||||
};
|
||||
|
||||
const onAddPost = async (bodyText) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
if (!routeSelector?.ownerBlockchainName || routeSelector.channelRootBlockNumber == null) {
|
||||
@@ -824,11 +1197,13 @@ export function render({ navigate, route }) {
|
||||
|
||||
const skeleton = renderSkeleton(screen);
|
||||
|
||||
let cleanupSeenTracking = null;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const apiData = await loadFromApi(route, channelId);
|
||||
skeleton.remove();
|
||||
renderBody(screen, navigate, routeKey, apiData, {
|
||||
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
|
||||
onToggleLike: async (messageRef, action) => {
|
||||
try {
|
||||
await onToggleLike(messageRef, action);
|
||||
@@ -853,6 +1228,7 @@ export function render({ navigate, route }) {
|
||||
throw new Error(toUserMessage(error, 'Не удалось добавить сообщение.'));
|
||||
}
|
||||
},
|
||||
onShare: onShare,
|
||||
onEditDescription: async (descriptionText) => {
|
||||
try {
|
||||
await onEditDescription(descriptionText);
|
||||
@@ -883,6 +1259,16 @@ export function render({ navigate, route }) {
|
||||
showStatus(toUserMessage(error, 'Не удалось отписаться от канала.'));
|
||||
}
|
||||
},
|
||||
onMarkSeenBatch: async (refs) => {
|
||||
try {
|
||||
await onMarkSeenBatch(refs);
|
||||
} catch (error) {
|
||||
throw new Error(toUserMessage(error, 'Не удалось отметить сообщения как прочитанные.'));
|
||||
}
|
||||
},
|
||||
onSeenError: (error) => {
|
||||
showStatus(toUserMessage(error, 'Не удалось обновить статус прочтения.'));
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
skeleton.remove();
|
||||
@@ -896,6 +1282,7 @@ export function render({ navigate, route }) {
|
||||
|
||||
screen.cleanup = () => {
|
||||
appScreen?.classList.remove('channels-scroll-clean');
|
||||
if (typeof cleanupSeenTracking === 'function') cleanupSeenTracking();
|
||||
};
|
||||
|
||||
return screen;
|
||||
|
||||
Reference in New Issue
Block a user