diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java index 68a3fed9..58ea1aa6 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java @@ -28,6 +28,7 @@ public final class SolanaUserPdaImportService { private static final HttpClient HTTP = HttpClient.newHttpClient(); private static final String MAGIC = "SHiNE"; private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 1; + private static final int ARCHIVE_HEAD_PAYLOAD_BYTES = 64; private SolanaUserPdaImportService() {} @@ -266,6 +267,8 @@ public final class SolanaUserPdaImportService { } } else if (blockType == 70) { c += 1; + } else if (blockType == 100) { + c += ARCHIVE_HEAD_PAYLOAD_BYTES; } else { return null; } @@ -374,6 +377,8 @@ public final class SolanaUserPdaImportService { } } else if (blockType == 70) { c += 1; + } else if (blockType == 100) { + c += ARCHIVE_HEAD_PAYLOAD_BYTES; } else { return null; } diff --git a/VERSION.properties b/VERSION.properties index b375809a..d54533b7 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.12.15 -server.version=1.10.6 +client.version=1.12.16 +server.version=1.10.7 diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index 4197f594..0201d3b0 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -306,6 +306,7 @@ function createChannelReadTracker({ let persistedSeenCount = safeInitialSeenCount; let initialPersistPending = !!initializeIfMissing; let inFlight = false; + let writeBlocked = false; let disposed = false; let rafId = 0; let timerId = 0; @@ -318,7 +319,7 @@ function createChannelReadTracker({ }; const queueFlush = (delayMs = 180) => { - if (disposed || !canWrite) return; + if (disposed || writeBlocked || !canWrite) return; clearTimer(); timerId = setTimeout(() => { timerId = 0; @@ -327,7 +328,7 @@ function createChannelReadTracker({ }; const flush = async () => { - if (disposed || !canWrite) return; + if (disposed || writeBlocked || !canWrite) return; const next = Math.max(safeInitialSeenCount, Math.min(desiredSeenCount, safeMessagesCount)); if (next <= persistedSeenCount && !initialPersistPending) return; if (inFlight) { @@ -358,6 +359,18 @@ function createChannelReadTracker({ }); } } catch (error) { + const errorCode = String(error?.code || '').trim().toUpperCase(); + if (errorCode === 'CHANNEL_NOT_FOLLOWED') { + // This is a persistent state mismatch, not a transient transport failure. + // Stop writes for the current channel view instead of hammering the server + // after every scroll/resize. A fresh view after subscribe/refresh will create + // a new tracker from the latest subscription state. + writeBlocked = true; + initialPersistPending = false; + clearTimer(); + if (typeof onPersistError === 'function') onPersistError(error, { retrying: false }); + return; + } if (typeof onPersistError === 'function') onPersistError(error, { retrying: true }); queueFlush(800); } finally { @@ -2411,46 +2424,54 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) { ? window.setTimeout(() => scrollChannelToUnreadLine(screen, unreadCount, false), 40) : 0; - const tracker = createChannelReadTracker({ - screen, - routeKey, - ownerBlockchainName: channelData.channel?.ownerBlockchainName || channelData.selector?.ownerBlockchainName, - channelName: channelData.channel?.name || channelData.channel?.channelName, - initializeIfMissing: !!(channelData.isSubscribed && !channelData.readStateInitialized), - unreadCount, - messagesCount, - initialSeenCount: readCount, - onPersistError: (error, options = {}) => { - const detail = String(error?.message || '').trim(); - const retryText = options?.retrying === false ? '' : ' Сервер повторит попытку автоматически.'; - showStatus(`Не удалось сохранить, сколько сообщений прочитано.${detail ? ` ${detail}` : ''}${retryText}`); - }, - onPersistSuccess: ({ readCount: persistedReadCount, unreadCount: persistedUnreadCount }) => { - channelData.readCount = persistedReadCount; - channelData.unreadCount = persistedUnreadCount; - channelData.readStateInitialized = true; + const shouldTrackReadState = !!( + channelData.isSubscribed + && !channelData.isOwnChannel + && !channelData.isDiary + ); - // 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 tracker = shouldTrackReadState + ? createChannelReadTracker({ + screen, + routeKey, + ownerBlockchainName: channelData.channel?.ownerBlockchainName || channelData.selector?.ownerBlockchainName, + channelName: channelData.channel?.name || channelData.channel?.channelName, + initializeIfMissing: !!(!channelData.readStateInitialized), + unreadCount, + messagesCount, + initialSeenCount: readCount, + onPersistError: (error, options = {}) => { + const detail = String(error?.message || '').trim(); + const retryText = options?.retrying === false ? '' : ' Сервер повторит попытку автоматически.'; + showStatus(`Не удалось сохранить, сколько сообщений прочитано.${detail ? ` ${detail}` : ''}${retryText}`); + }, + onPersistSuccess: ({ readCount: persistedReadCount, unreadCount: persistedUnreadCount }) => { + channelData.readCount = persistedReadCount; + channelData.unreadCount = persistedUnreadCount; + channelData.readStateInitialized = true; - 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; + // 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(''); - }, - }); + showStatus(''); + }, + }) + : { cleanup() {}, measure() {} }; return () => { if (pendingScrollTimer) window.clearTimeout(pendingScrollTimer);