Исправить синхронизацию прочитанных каналов

This commit is contained in:
AidarKC
2026-09-10 12:33:24 +03:00
parent 107f85b818
commit 4c7e71f21f
11 changed files with 125 additions and 60 deletions
+62 -17
View File
@@ -292,10 +292,14 @@ function createChannelReadTracker({
const storagePwd = state.session.storagePwdInMemory;
const cleanOwnerBlockchainName = String(ownerBlockchainName || '').trim();
const cleanChannelName = String(channelName || '').trim();
const canWrite = !!(cleanOwnerBlockchainName && cleanChannelName && login && storagePwd);
const missingWriteParts = [];
if (!cleanOwnerBlockchainName) missingWriteParts.push('ownerBlockchainName');
if (!cleanChannelName) missingWriteParts.push('channelName');
if (!login) missingWriteParts.push('login');
if (!storagePwd) missingWriteParts.push('storagePwd');
const canWrite = missingWriteParts.length === 0;
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;
const unreadAnchorFraction = getUnreadAnchorViewportFraction(unreadCount);
let desiredSeenCount = safeInitialSeenCount;
@@ -333,7 +337,7 @@ function createChannelReadTracker({
inFlight = true;
try {
await authService.setChannelReadState({
const persisted = await authService.setChannelReadState({
login,
ownerBlockchainName: cleanOwnerBlockchainName,
channelName: cleanChannelName,
@@ -341,11 +345,20 @@ function createChannelReadTracker({
timeMs: Date.now(),
storagePwd,
});
persistedSeenCount = next;
const serverReadCount = Math.max(0, Number(persisted?.read_count ?? next));
const serverUnreadCount = Math.max(0, Number(persisted?.unread_count ?? Math.max(0, safeMessagesCount - serverReadCount)));
persistedSeenCount = Math.max(persistedSeenCount, Math.min(serverReadCount, safeMessagesCount));
desiredSeenCount = Math.max(desiredSeenCount, persistedSeenCount);
initialPersistPending = false;
if (typeof onPersistSuccess === 'function') onPersistSuccess(persistedSeenCount);
if (typeof onPersistSuccess === 'function') {
onPersistSuccess({
readCount: persistedSeenCount,
unreadCount: serverUnreadCount,
applied: persisted?.applied !== false,
});
}
} catch (error) {
if (typeof onPersistError === 'function') onPersistError(error);
if (typeof onPersistError === 'function') onPersistError(error, { retrying: true });
queueFlush(800);
} finally {
inFlight = false;
@@ -355,7 +368,9 @@ function createChannelReadTracker({
const collectSeenCount = () => {
const cards = Array.from(screen.querySelectorAll('.channel-message-card[data-local-number]'));
if (!cards.length) return safeInitialSeenCount;
if (!unreadLine) return safeMessagesCount;
// Absence of the divider must never imply that unseen messages are read.
// Use only the highest message card that actually crossed the viewport.
const root = getChannelScrollRoot();
const viewportHeight = root?.clientHeight || window.innerHeight || document.documentElement.clientHeight || 0;
@@ -389,6 +404,12 @@ function createChannelReadTracker({
});
};
if (!canWrite && (initializeIfMissing || unreadCount > 0)) {
const error = new Error(`Нельзя сохранить прочитанность канала: отсутствует ${missingWriteParts.join(', ')}`);
console.error('[SHiNE][ChannelReadState]', error);
if (typeof onPersistError === 'function') onPersistError(error, { retrying: false });
}
const scrollRoot = getChannelScrollRoot();
const onScroll = () => measure();
const onResize = () => measure();
@@ -1462,6 +1483,7 @@ async function loadFromApi(route, channelId) {
const isAuthorized = !!currentSessionLogin;
let unreadCount = 0;
let messagesCount = 0;
let readCount = 0;
let readStateInitialized = false;
let cachedFeed = null;
const ensureFeed = async () => {
@@ -1523,9 +1545,6 @@ async function loadFromApi(route, channelId) {
}
if (selector?.ownerBlockchainName && selector?.channelName) {
let unreadCount = 0;
let messagesCount = 0;
const routeOwnerRaw = String(selector.ownerBlockchainName || '').trim();
const routeOwnerNormalized = routeOwnerRaw.toLowerCase();
const routeOwnerLoginFromBch = extractLoginFromBlockchainName(routeOwnerRaw);
@@ -1577,6 +1596,7 @@ async function loadFromApi(route, channelId) {
}
unreadCount = Number(channel?.unreadCount || 0);
messagesCount = Number(channel?.messagesCount || 0);
readCount = Number(channel?.readCount || 0);
readStateInitialized = !!channel?.readStateInitialized;
selector = {
ownerBlockchainName: String(channel.channel.ownerBlockchainName),
@@ -1590,7 +1610,7 @@ async function loadFromApi(route, channelId) {
throw new Error('Не удалось определить канал из адреса страницы.');
}
const payload = await authService.getChannelMessages(selector, 200, 'asc', currentSessionLogin);
const payload = await authService.getChannelMessages(selector, null, 'asc', currentSessionLogin);
const messages = Array.isArray(payload.messages) ? payload.messages : [];
let reverseChannelMissingWarning = '';
let mergedMessages = [...messages];
@@ -1625,7 +1645,7 @@ async function loadFromApi(route, channelId) {
channelRootBlockNumber: Number(reverseSummary.channel.channelRoot.blockNumber),
channelRootBlockHash: normalizeRouteHash(reverseSummary.channel.channelRoot.blockHash),
};
const reversePayload = await authService.getChannelMessages(reverseSelector, 200, 'asc', currentSessionLogin);
const reversePayload = await authService.getChannelMessages(reverseSelector, null, 'asc', currentSessionLogin);
const reverseMessages = Array.isArray(reversePayload?.messages) ? reversePayload.messages : [];
mergedMessages = mergedMessages.concat(reverseMessages);
} else {
@@ -1669,6 +1689,7 @@ async function loadFromApi(route, channelId) {
reverseChannelMissingWarning,
unreadCount,
messagesCount,
readCount,
isOwnChannel,
isSubscribed,
readStateInitialized,
@@ -2276,7 +2297,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
const showStatus = typeof handlers?.showStatus === 'function' ? handlers.showStatus : () => {};
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);
const readCount = Math.max(0, Math.min(Number(channelData.readCount || 0), messagesCount));
if (channelData.reverseChannelMissingWarning) {
const reverseWarning = document.createElement('p');
@@ -2328,7 +2349,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
if (!unreadLineInserted && item.type === 'post' && Number(item?.post?.localNumber || 0) > readCount) {
const unreadLine = document.createElement('div');
unreadLine.className = 'card channel-unread-line';
unreadLine.textContent = 'Не прочитанные сообщения';
unreadLine.textContent = 'Новые сообщения';
feed.append(unreadLine);
unreadLineInserted = true;
}
@@ -2399,10 +2420,34 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
unreadCount,
messagesCount,
initialSeenCount: readCount,
onPersistError: () => {
showStatus('Не удалось сохранить, сколько сообщений прочитано. Сервер повторит попытку автоматически.');
onPersistError: (error, options = {}) => {
const detail = String(error?.message || '').trim();
const retryText = options?.retrying === false ? '' : ' Сервер повторит попытку автоматически.';
showStatus(`Не удалось сохранить, сколько сообщений прочитано.${detail ? ` ${detail}` : ''}${retryText}`);
},
onPersistSuccess: () => {
onPersistSuccess: ({ readCount: persistedReadCount, unreadCount: persistedUnreadCount }) => {
channelData.readCount = persistedReadCount;
channelData.unreadCount = persistedUnreadCount;
channelData.readStateInitialized = true;
// The "Новые сообщения" divider is a snapshot of the unread boundary at the
// moment this channel view was opened. Persisting read state must not move or
// remove it during the current view session; reopening the channel recalculates it.
const feedGroups = ['ownedChannels', 'followedUsersChannels', 'followedChannels'];
for (const group of feedGroups) {
const rows = Array.isArray(state.channelsFeed?.[group]) ? state.channelsFeed[group] : [];
const row = rows.find((item) => (
String(item?.channel?.ownerBlockchainName || '') === String(channelData.selector?.ownerBlockchainName || '')
&& Number(item?.channel?.channelRoot?.blockNumber) === Number(channelData.selector?.channelRootBlockNumber)
&& normalizeRouteHash(item?.channel?.channelRoot?.blockHash) === normalizeRouteHash(channelData.selector?.channelRootBlockHash)
));
if (row) {
row.readCount = persistedReadCount;
row.unreadCount = persistedUnreadCount;
row.readStateInitialized = true;
}
}
showStatus('');
},
});