SHA256
Restore unread baseline and read tracking
This commit is contained in:
@@ -241,6 +241,183 @@ function buildChannelSettingsKey(ownerBlockchainName, channelName) {
|
||||
return `${ownerBch}/${name}`;
|
||||
}
|
||||
|
||||
function getChannelScrollRoot() {
|
||||
return document.getElementById('app-screen');
|
||||
}
|
||||
|
||||
function scrollRootBy(delta, smooth = false) {
|
||||
const root = getChannelScrollRoot();
|
||||
const behavior = smooth ? 'smooth' : 'auto';
|
||||
if (root && typeof root.scrollBy === 'function') {
|
||||
root.scrollBy({ top: delta, behavior });
|
||||
return;
|
||||
}
|
||||
window.scrollBy({ top: delta, behavior });
|
||||
}
|
||||
|
||||
function scrollElementToViewportFraction(element, fraction = 1 / 3, smooth = false) {
|
||||
if (!element) return false;
|
||||
const root = getChannelScrollRoot();
|
||||
const viewportHeight = root?.clientHeight || window.innerHeight || document.documentElement.clientHeight || 0;
|
||||
const targetTop = Math.max(0, Math.round(viewportHeight * fraction));
|
||||
const rect = element.getBoundingClientRect();
|
||||
const delta = rect.top - targetTop;
|
||||
if (Math.abs(delta) < 2) return true;
|
||||
scrollRootBy(delta, smooth);
|
||||
return true;
|
||||
}
|
||||
|
||||
function scrollChannelToUnreadLine(screen, smooth = false) {
|
||||
return scrollElementToViewportFraction(screen.querySelector('.channel-unread-line'), 1 / 3, smooth);
|
||||
}
|
||||
|
||||
function createChannelReadTracker({
|
||||
screen,
|
||||
routeKey,
|
||||
settingKey,
|
||||
unreadCount,
|
||||
messagesCount,
|
||||
initialSeenCount,
|
||||
}) {
|
||||
const login = String(state.session.login || '').trim();
|
||||
const storagePwd = state.session.storagePwdInMemory;
|
||||
const canWrite = !!(settingKey && login && storagePwd);
|
||||
const safeMessagesCount = Math.max(0, Number(messagesCount || 0));
|
||||
const safeInitialSeenCount = Math.max(0, Math.min(Number(initialSeenCount || 0), safeMessagesCount));
|
||||
const unreadLine = unreadCount > 0 ? screen.querySelector('.channel-unread-line') : null;
|
||||
|
||||
let desiredSeenCount = safeInitialSeenCount;
|
||||
let persistedSeenCount = safeInitialSeenCount;
|
||||
let inFlight = false;
|
||||
let disposed = false;
|
||||
let rafId = 0;
|
||||
let timerId = 0;
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
timerId = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const queueFlush = (delayMs = 180) => {
|
||||
if (disposed || !canWrite) return;
|
||||
clearTimer();
|
||||
timerId = setTimeout(() => {
|
||||
timerId = 0;
|
||||
void flush();
|
||||
}, Math.max(0, Number(delayMs) || 0));
|
||||
};
|
||||
|
||||
const flush = async () => {
|
||||
if (disposed || !canWrite) return;
|
||||
const next = Math.max(safeInitialSeenCount, Math.min(desiredSeenCount, safeMessagesCount));
|
||||
if (next <= persistedSeenCount) return;
|
||||
if (inFlight) {
|
||||
queueFlush(120);
|
||||
return;
|
||||
}
|
||||
|
||||
inFlight = true;
|
||||
try {
|
||||
await authService.upsertUserSetting({
|
||||
login,
|
||||
settingType: 1,
|
||||
settingKey,
|
||||
timeMs: Date.now(),
|
||||
valueText: '',
|
||||
valueNum: next,
|
||||
storagePwd,
|
||||
});
|
||||
persistedSeenCount = next;
|
||||
} catch {
|
||||
queueFlush(800);
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
const collectSeenCount = () => {
|
||||
const cards = Array.from(screen.querySelectorAll('.channel-message-card[data-local-number]'));
|
||||
if (!cards.length) return safeInitialSeenCount;
|
||||
if (!unreadLine) return safeMessagesCount;
|
||||
|
||||
const root = getChannelScrollRoot();
|
||||
const viewportHeight = root?.clientHeight || window.innerHeight || document.documentElement.clientHeight || 0;
|
||||
const thresholdTop = Math.max(0, Math.round(viewportHeight / 3));
|
||||
let seen = safeInitialSeenCount;
|
||||
for (const card of cards) {
|
||||
const localNumber = Number(card.dataset.localNumber || 0);
|
||||
if (!Number.isFinite(localNumber) || localNumber <= 0) continue;
|
||||
const rect = card.getBoundingClientRect();
|
||||
if (rect.top > thresholdTop + 1) break;
|
||||
seen = Math.max(seen, localNumber);
|
||||
}
|
||||
return Math.max(safeInitialSeenCount, Math.min(seen, safeMessagesCount));
|
||||
};
|
||||
|
||||
const measure = () => {
|
||||
if (disposed) return;
|
||||
if (rafId) return;
|
||||
rafId = window.requestAnimationFrame(() => {
|
||||
rafId = 0;
|
||||
const next = collectSeenCount();
|
||||
if (next > desiredSeenCount) {
|
||||
desiredSeenCount = next;
|
||||
queueFlush(180);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const scrollRoot = getChannelScrollRoot();
|
||||
const onScroll = () => measure();
|
||||
const onResize = () => measure();
|
||||
|
||||
if (scrollRoot && typeof scrollRoot.addEventListener === 'function') {
|
||||
scrollRoot.addEventListener('scroll', onScroll, { passive: true });
|
||||
} else {
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
const initialSyncRequired = canWrite && unreadCount <= 0 && safeMessagesCount >= 0;
|
||||
if (initialSyncRequired) {
|
||||
void authService.upsertUserSetting({
|
||||
login,
|
||||
settingType: 1,
|
||||
settingKey,
|
||||
timeMs: Date.now(),
|
||||
valueText: '',
|
||||
valueNum: safeMessagesCount,
|
||||
storagePwd,
|
||||
}).catch(() => {});
|
||||
persistedSeenCount = safeMessagesCount;
|
||||
desiredSeenCount = safeMessagesCount;
|
||||
} else {
|
||||
window.setTimeout(() => measure(), 120);
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
disposed = true;
|
||||
clearTimer();
|
||||
if (rafId) {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
rafId = 0;
|
||||
}
|
||||
if (scrollRoot && typeof scrollRoot.removeEventListener === 'function') {
|
||||
scrollRoot.removeEventListener('scroll', onScroll);
|
||||
} else {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
measure,
|
||||
};
|
||||
}
|
||||
|
||||
function firstNonEmptyText(...candidates) {
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate !== 'string') continue;
|
||||
@@ -1751,6 +1928,9 @@ function renderPostCard(post, {
|
||||
if (refKey) {
|
||||
card.dataset.messageKey = refKey;
|
||||
}
|
||||
if (Number.isFinite(Number(post.localNumber)) && Number(post.localNumber) > 0) {
|
||||
card.dataset.localNumber = String(Number(post.localNumber));
|
||||
}
|
||||
card.classList.add('is-counters-visible');
|
||||
|
||||
if (!post.messageRef || !selector) return card;
|
||||
@@ -1916,6 +2096,10 @@ function renderPostCard(post, {
|
||||
}
|
||||
|
||||
function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
const unreadCount = Math.max(0, Number(channelData.unreadCount || 0));
|
||||
const messagesCount = Math.max(0, Number(channelData.messagesCount || (Array.isArray(channelData.posts) ? channelData.posts.length : 0) || 0));
|
||||
const readCount = Math.max(0, messagesCount - unreadCount);
|
||||
|
||||
if (channelData.reverseChannelMissingWarning) {
|
||||
const reverseWarning = document.createElement('p');
|
||||
reverseWarning.className = 'channel-head-meta';
|
||||
@@ -1923,13 +2107,6 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
screen.append(reverseWarning);
|
||||
}
|
||||
|
||||
if (Number(channelData.unreadCount || 0) > 0) {
|
||||
const unreadLine = document.createElement('div');
|
||||
unreadLine.className = 'card channel-unread-line';
|
||||
unreadLine.textContent = `Не прочитано: ${channelData.unreadCount}`;
|
||||
screen.append(unreadLine);
|
||||
}
|
||||
|
||||
const actionButton = document.createElement('button');
|
||||
actionButton.className = 'destructive-btn channel-main-action';
|
||||
actionButton.textContent = 'Подписаться на канал';
|
||||
@@ -1948,6 +2125,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
const postsByKey = new Map();
|
||||
const metaEvents = (Array.isArray(channelData.metaEvents) ? channelData.metaEvents : [])
|
||||
.map((event) => mapChannelMetaEvent(event, channelData.channel));
|
||||
let unreadLineInserted = unreadCount === 0;
|
||||
const feedItems = [
|
||||
...metaEvents.map((event) => ({
|
||||
type: 'meta',
|
||||
@@ -1969,6 +2147,13 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
|
||||
if (feedItems.length) {
|
||||
feedItems.forEach((item) => {
|
||||
if (!unreadLineInserted && item.type === 'post' && Number(item?.post?.localNumber || 0) > readCount) {
|
||||
const unreadLine = document.createElement('div');
|
||||
unreadLine.className = 'card channel-unread-line';
|
||||
unreadLine.textContent = `Не прочитано: ${unreadCount}`;
|
||||
feed.append(unreadLine);
|
||||
unreadLineInserted = true;
|
||||
}
|
||||
if (item.type === 'meta') {
|
||||
feed.append(renderChannelMetaEventCard(item.event));
|
||||
return;
|
||||
@@ -2020,10 +2205,25 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
screen.append(feed, backButton);
|
||||
}
|
||||
|
||||
applyPendingScroll(screen, routeKey, channelData.isOwnChannel || channelData.isDiary || Number(channelData.unreadCount || 0) === 0);
|
||||
return () => {
|
||||
// noop
|
||||
};
|
||||
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, false), 40);
|
||||
}
|
||||
|
||||
const tracker = createChannelReadTracker({
|
||||
screen,
|
||||
routeKey,
|
||||
settingKey: buildChannelSettingsKey(
|
||||
channelData.channel?.ownerBlockchainName || channelData.selector?.ownerBlockchainName,
|
||||
channelData.channel?.name || channelData.channel?.channelName,
|
||||
),
|
||||
unreadCount,
|
||||
messagesCount,
|
||||
initialSeenCount: readCount,
|
||||
});
|
||||
|
||||
return tracker.cleanup;
|
||||
}
|
||||
|
||||
function renderSkeleton(screen) {
|
||||
@@ -2319,22 +2519,6 @@ export function render({ navigate, route, chrome }) {
|
||||
try {
|
||||
const apiData = await loadFromApi(route, channelId);
|
||||
activeSelector = apiData?.selector || null;
|
||||
const lastSeenCount = Number(apiData?.messagesCount || (Array.isArray(apiData?.posts) ? apiData.posts.length : 0) || 0);
|
||||
const settingKey = buildChannelSettingsKey(
|
||||
apiData?.channel?.ownerBlockchainName || apiData?.selector?.ownerBlockchainName,
|
||||
apiData?.channel?.name || apiData?.channel?.channelName,
|
||||
);
|
||||
if (settingKey && state.session.login && state.session.storagePwdInMemory) {
|
||||
void authService.upsertUserSetting({
|
||||
login: state.session.login,
|
||||
settingType: 1,
|
||||
settingKey,
|
||||
timeMs: Date.now(),
|
||||
valueText: '',
|
||||
valueNum: lastSeenCount,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
}).catch(() => {});
|
||||
}
|
||||
const titleLabel = apiData?.channel?.displayTitle || apiData?.channel?.name || 'channel';
|
||||
const entrypointPosts = getEntrypointPosts(apiData?.posts);
|
||||
const openEntrypointHistory = () => {
|
||||
|
||||
Reference in New Issue
Block a user