diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ChannelReadStateDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ChannelReadStateDAO.java index b5d0825c..14ddb017 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ChannelReadStateDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ChannelReadStateDAO.java @@ -136,6 +136,23 @@ public final class ChannelReadStateDAO { } } + + public Integer resolvePublicChannelRootByName(Connection c, String ownerBchName, String channelName) throws SQLException { + String sql = """ + SELECT channel_root_block_number + FROM channel_names_state + WHERE owner_bch_name=? AND slug=? AND channel_type_code=1 + LIMIT 1 + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, ownerBchName); + ps.setString(2, channelName); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt("channel_root_block_number") : null; + } + } + } + public String resolveOwnerLogin(Connection c, String ownerBchName, String channelName) throws SQLException { String sql = """ SELECT owner_login diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java index bd3ac25a..762d8fba 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java @@ -236,12 +236,13 @@ final class ChannelsReadSupport { static List channelPosts(Connection c, String ownerBch, int lineCode, int limit, boolean asc) throws SQLException { String order = asc ? "ASC" : "DESC"; + boolean bounded = limit > 0; String sql = """ SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,msg_sub_type,this_line_number FROM blocks WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?, ?, ?, ?, ?) AND line_code=? ORDER BY block_number - """ + order + " LIMIT ?"; + """ + order + (bounded ? " LIMIT ?" : ""); try (PreparedStatement ps = c.prepareStatement(sql)) { ps.setString(1, ownerBch); ps.setInt(2, MSG_TYPE_TEXT); @@ -252,7 +253,7 @@ final class ChannelsReadSupport { ps.setInt(7, MsgSubType.TEXT_SERVICE); ps.setInt(8, MsgSubType.TEXT_COURSE); ps.setInt(9, lineCode); - ps.setInt(10, limit); + if (bounded) ps.setInt(10, limit); try (ResultSet rs = ps.executeQuery()) { List out = new ArrayList<>(); while (rs.next()) { diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java index bb95d644..f3c6e71c 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java @@ -34,9 +34,11 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler { return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля channel"); } - int limit = req.getLimit() == null ? 30 : req.getLimit(); - if (limit <= 0 || limit > 1000) { - return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "limit_too_large", "Некорректный limit"); + // limit is optional. null/0 means "return the whole channel". + // Positive values are kept for backwards-compatible callers that still want a bounded response. + int limit = req.getLimit() == null ? 0 : req.getLimit(); + if (limit < 0) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_limit", "Некорректный limit"); } boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort()); @@ -113,7 +115,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler { if (!asc) { java.util.Collections.reverse(posts); } - if (posts.size() > limit) { + if (limit > 0 && posts.size() > limit) { posts = new ArrayList<>(posts.subList(0, limit)); } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java index 394215c4..797295a8 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java @@ -82,12 +82,14 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler { channelRef.setChannelRoot(rootRef); row.setChannel(channelRef); - row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber)); + int messagesCount = ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber); + row.setMessagesCount(messagesCount); boolean ownChannel = key.ownerLogin != null && key.ownerLogin.equalsIgnoreCase(viewerLogin); ChannelReadStateDAO.ReadState readState = ownChannel || meta.channelName == null ? null : ChannelReadStateDAO.getInstance().get(c, viewerLogin, key.ownerBch, meta.channelName); row.setReadStateInitialized(readState != null); + row.setReadCount(readState == null ? 0L : Math.min(messagesCount, Math.max(0L, readState.readCount()))); row.setUnreadCount(readState == null ? 0 : (int) Math.min(Integer.MAX_VALUE, readState.unreadCount())); ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_SetChannelReadState_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_SetChannelReadState_Handler.java index 2c960e76..cefc6df3 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_SetChannelReadState_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_SetChannelReadState_Handler.java @@ -100,13 +100,21 @@ public final class Net_SetChannelReadState_Handler implements JsonMessageHandler "CHANNEL_NOT_FOLLOWED", "Пользователь не подписан на этот канал"); } String ownerLogin = dao.resolveOwnerLogin(c, ownerBch, channelName); - if (ownerLogin == null || ownerLogin.isBlank()) { + Integer channelRoot = dao.resolvePublicChannelRootByName(c, ownerBch, channelName); + if (ownerLogin == null || ownerLogin.isBlank() || channelRoot == null) { return NetExceptionResponseFactory.error(req, 404, "CHANNEL_NOT_FOUND", "Канал не найден"); } + + // Never persist a watermark beyond the number of messages that really exists. + // Without this guard, one buggy/malicious oversized read_count would be monotonic + // and could make all future unread counters impossible to clear correctly. + long actualMessagesCount = ChannelsReadSupport.countPosts(c, ownerBch, channelRoot); + long effectiveReadCount = Math.min(readCount, actualMessagesCount); + result = dao.upsertSignedReadIfNewer(c, authenticatedLogin, ownerLogin, ownerBch, channelName, - readCount, timeMs, clientKeyB64, signatureB64, nowMs); + effectiveReadCount, timeMs, clientKeyB64, signatureB64, nowMs); } Net_SetChannelReadState_Response resp = new Net_SetChannelReadState_Response(); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java index 1f163dea..457e3f2e 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java @@ -26,6 +26,7 @@ public class Net_ListSubscriptionsFeed_Response extends Net_Response { public static class ChannelSummary { private ChannelRef channel; private int messagesCount; + private long readCount; private int unreadCount; private boolean readStateInitialized; private LastMessage lastMessage; @@ -36,6 +37,9 @@ public class Net_ListSubscriptionsFeed_Response extends Net_Response { public int getMessagesCount() { return messagesCount; } public void setMessagesCount(int messagesCount) { this.messagesCount = messagesCount; } + public long getReadCount() { return readCount; } + public void setReadCount(long readCount) { this.readCount = readCount; } + public int getUnreadCount() { return unreadCount; } public void setUnreadCount(int unreadCount) { this.unreadCount = unreadCount; } diff --git a/VERSION.properties b/VERSION.properties index 6d57c3ce..d978ef40 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.12.9 -server.version=1.10.3 +client.version=1.12.10 +server.version=1.10.4 diff --git a/docs/API/06_Channels_Read_API.md b/docs/API/06_Channels_Read_API.md index 99172efd..33da7c92 100644 --- a/docs/API/06_Channels_Read_API.md +++ b/docs/API/06_Channels_Read_API.md @@ -29,9 +29,9 @@ > На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки. > -> `unreadCount` для канала считается по `user_settings`. +> `unreadCount` для канала считается по подписанному состоянию чтения канала. > Для собственных каналов владельцу всегда возвращается `unreadCount = 0`, чтобы его собственные публикации не становились «новыми» для него самого. -> Если для пары `ownerBlockchainName/channelName` ещё нет записи, канал временно считается полностью прочитанным; UI при загрузке списка каналов создаёт baseline на текущем `messagesCount`. После этого новые публикации увеличивают `unreadCount` до продвижения курсора чтения. +> Если для пары `ownerBlockchainName/channelName` ещё нет записи, канал временно считается полностью прочитанным. После появления записи новые публикации увеличивают `unreadCount` до продвижения курсора чтения. --- @@ -112,6 +112,7 @@ "channelRoot": { "blockNumber": 456, "blockHash": "..." } }, "messagesCount": 90, + "readCount": 0, "unreadCount": 0, "readStateInitialized": false, "lastMessage": { @@ -148,6 +149,8 @@ } ``` +`limit` необязателен. Если поле отсутствует или равно `0`, сервер возвращает всю ленту канала. Положительное значение ограничивает количество сообщений для совместимых клиентов. + ### Response (success) ```json { @@ -539,7 +542,7 @@ SHiNe/ChannelReadState:|||| 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(''); }, }); diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index 397dcb0f..746a1999 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -902,7 +902,6 @@ export function render({ navigate, route, chrome }) { const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase()); let peerRelationType = isKnownContact ? 'contact' : 'none'; const hasUnreadIncoming = getChatMessages(chatId).some((msg) => msg?.from === 'in' && msg?.unread); - const UNREAD_SEPARATOR_AUTO_HIDE_MS = 5000; let historyHasMore = true; let historyLoading = false; let historyNextBeforeTimeMs = 0; @@ -910,16 +909,8 @@ export function render({ navigate, route, chrome }) { let historyBootstrapped = false; let boundScrollContainer = null; let unreadSeparatorVisible = hasUnreadIncoming; - let unreadSeparatorHideTimer = null; let unreadSeparatorAnchorMessageKey = getChatMessages(chatId).find((msg) => msg?.from === 'in' && msg?.unread)?.messageKey || ''; - const clearUnreadSeparatorHideTimer = () => { - if (unreadSeparatorHideTimer) { - window.clearTimeout(unreadSeparatorHideTimer); - unreadSeparatorHideTimer = null; - } - }; - const renderChatLog = ({ scrollMode = 'latest', markAsRead = true } = {}) => { renderLog(log, chatId, { onOpenActions: handleOpenActions, @@ -928,13 +919,9 @@ export function render({ navigate, route, chrome }) { showUnreadSeparator: unreadSeparatorVisible, unreadSeparatorMessageKey: unreadSeparatorAnchorMessageKey, }); - if (unreadSeparatorVisible) { - scheduleUnreadSeparatorAutoHide(); - } }; const hideUnreadSeparator = ({ rerender = true } = {}) => { - clearUnreadSeparatorHideTimer(); if (!unreadSeparatorVisible) return; unreadSeparatorVisible = false; if (rerender) { @@ -942,14 +929,6 @@ export function render({ navigate, route, chrome }) { } }; - const scheduleUnreadSeparatorAutoHide = () => { - clearUnreadSeparatorHideTimer(); - if (!unreadSeparatorVisible) return; - unreadSeparatorHideTimer = window.setTimeout(() => { - hideUnreadSeparator({ rerender: true }); - }, UNREAD_SEPARATOR_AUTO_HIDE_MS); - }; - const handleReadAloud = async (msg) => { if (!isTextToSpeechConfigured(state.entrySettings)) { showTtsMissingConfigDialog(); @@ -1371,7 +1350,6 @@ export function render({ navigate, route, chrome }) { const safeText = sanitizeUserDmTextForSend(String(rawText || '')); const text = safeText.trim(); if (!text) return; - hideUnreadSeparator({ rerender: false }); const editing = activeEdit; const replying = !editing ? activeReply : null; const finalText = editing @@ -1414,6 +1392,13 @@ export function render({ navigate, route, chrome }) { deliveryState: result?.deliveryState || 'accepted', }); + // In a DM the "Новые сообщения" divider stays fixed for the current open + // chat session and disappears only after a successfully sent NEW message. + // Editing an existing message must not clear the divider. + if (!editing) { + hideUnreadSeparator({ rerender: false }); + } + if (editing) { cancelEditMode({ restoreDraft: true }); } else if (replying) { @@ -1634,7 +1619,6 @@ export function render({ navigate, route, chrome }) { unreadSeparatorAnchorMessageKey = getChatMessages(chatId).find((msg) => msg?.from === 'in' && msg?.unread)?.messageKey || unreadSeparatorAnchorMessageKey; unreadSeparatorVisible = Boolean(unreadSeparatorAnchorMessageKey); } - scheduleUnreadSeparatorAutoHide(); } preserveComposerSelection(input, () => { renderChatLog({ scrollMode: 'latest' }); @@ -1660,10 +1644,8 @@ export function render({ navigate, route, chrome }) { markAsRead: false, scrollMode: hasUnreadIncoming ? 'unread' : 'latest', showUnreadSeparator: unreadSeparatorVisible, + unreadSeparatorMessageKey: unreadSeparatorAnchorMessageKey, }); - if (unreadSeparatorVisible) { - scheduleUnreadSeparatorAutoHide(); - } window.setTimeout(() => { if (markChatRead(chatId) > 0) { notifyUnreadStateUpdated(); @@ -1681,7 +1663,6 @@ export function render({ navigate, route, chrome }) { window.removeEventListener('shine-chat-messages-updated', handleIncomingChatRefresh); window.removeEventListener('shine-dm-delivery-updated', handleDeliveryRefresh); boundScrollContainer?.removeEventListener('scroll', handleHistoryScroll); - clearUnreadSeparatorHideTimer(); }; return screen; } diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index f1687c1f..3f4124f8 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -1574,13 +1574,15 @@ export class AuthService { return response.payload || {}; } - async getChannelMessages(channel, limit = 200, sort = 'asc', login = '') { + async getChannelMessages(channel, limit = null, sort = 'asc', login = '') { const normalizedChannel = { ownerBlockchainName: String(channel?.ownerBlockchainName || '').trim(), channelRootBlockNumber: Number(channel?.channelRootBlockNumber), channelRootBlockHash: String(channel?.channelRootBlockHash || '').trim(), }; - const payload = { channel: normalizedChannel, limit, sort }; + const payload = { channel: normalizedChannel, sort }; + const cleanLimit = Number(limit); + if (Number.isFinite(cleanLimit) && cleanLimit > 0) payload.limit = Math.trunc(cleanLimit); const cleanLogin = String(login || '').trim(); if (cleanLogin) payload.login = cleanLogin; const response = await this.ws.request('GetChannelMessages', payload);