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 b203ba94..faebbb6c 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,7 +82,10 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler { row.setChannel(channelRef); row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber)); - row.setUnreadCount(ChannelsReadSupport.countUnreadMessages(c, viewerLogin, key.ownerBch, meta.channelName, row.getMessagesCount())); + boolean ownChannel = key.ownerLogin != null && key.ownerLogin.equalsIgnoreCase(viewerLogin); + row.setUnreadCount(ownChannel + ? 0 + : ChannelsReadSupport.countUnreadMessages(c, viewerLogin, key.ownerBch, meta.channelName, row.getMessagesCount())); ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber); if (lastPost != null) { diff --git a/docs/API/06_Channels_Read_API.md b/docs/API/06_Channels_Read_API.md index cfe20e5a..62b97634 100644 --- a/docs/API/06_Channels_Read_API.md +++ b/docs/API/06_Channels_Read_API.md @@ -24,8 +24,9 @@ > На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки. > -> `unreadCount` для канала считается по `user_settings`: -> если для пары `ownerBlockchainName/channelName` ещё нет записи, канал считается полностью прочитанным, а UI при первом открытии записывает текущий курсор чтения. +> `unreadCount` для канала считается по `user_settings`. +> Для собственных каналов владельцу всегда возвращается `unreadCount = 0`, чтобы его собственные публикации не становились «новыми» для него самого. +> Если для пары `ownerBlockchainName/channelName` ещё нет записи, канал временно считается полностью прочитанным; UI при загрузке списка каналов создаёт baseline на текущем `messagesCount`. После этого новые публикации увеличивают `unreadCount` до продвижения курсора чтения. --- diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index 4dd19819..27a359ad 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -393,22 +393,11 @@ function createChannelReadTracker({ } 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; + if (canWrite) { desiredSeenCount = safeMessagesCount; - } else { - window.setTimeout(() => measure(), 120); + void flush(); } + window.setTimeout(() => measure(), 120); const cleanup = () => { disposed = true; diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index 622fe6b4..9ba02eb0 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -27,6 +27,7 @@ const CHANNEL_TYPE_STORIES = 0; const CHANNEL_TYPE_PERSONAL = 100; const DIARY_CHANNEL_NAME = 'diary'; const DIARY_DISPLAY_NAME = 'Дневник'; +const CHANNEL_READ_SETTING_TYPE = 1; const CHANNELS_VIEW_ALL = 'all'; const CHANNELS_VIEW_OWNED = 'owned'; @@ -111,6 +112,58 @@ function isVisibleChannelSummary(summary) { return !!ownerLogin && !!channelName; } +function channelReadSettingKey(summary) { + const ownerBch = String(summary?.channel?.ownerBlockchainName || '').trim(); + const channelName = String(summary?.channel?.channelName || '').trim(); + if (!ownerBch || !channelName) return ''; + return `${ownerBch}/${channelName}`; +} + +async function ensureChannelReadBaselines(feed) { + const login = String(state.session.login || '').trim(); + const storagePwd = state.session.storagePwdInMemory; + if (!login || !storagePwd) return; + + let settingsPayload; + try { + settingsPayload = await authService.listUserSettings(login); + } catch { + return; + } + + const existing = new Set( + (Array.isArray(settingsPayload?.settings) ? settingsPayload.settings : []) + .filter((item) => Number(item?.setting_type) === CHANNEL_READ_SETTING_TYPE) + .map((item) => String(item?.setting_key || '').trim()) + .filter(Boolean), + ); + + const summaries = [ + ...(Array.isArray(feed?.followedUsersChannels) ? feed.followedUsersChannels : []), + ...(Array.isArray(feed?.followedChannels) ? feed.followedChannels : []), + ].filter(isVisibleChannelSummary); + + for (const summary of summaries) { + const settingKey = channelReadSettingKey(summary); + if (!settingKey || existing.has(settingKey)) continue; + + try { + await authService.upsertUserSetting({ + login, + settingType: CHANNEL_READ_SETTING_TYPE, + settingKey, + timeMs: Date.now(), + valueText: '', + valueNum: Math.max(0, Number(summary?.messagesCount || 0)), + storagePwd, + }); + existing.add(settingKey); + } catch { + // Не ломаем экран каналов из-за фоновой инициализации read-state. + } + } +} + function avatarLetterFromName(name = '') { const first = Array.from(String(name || '').trim())[0] || '#'; return first.toUpperCase(); @@ -1323,6 +1376,7 @@ async function loadFeedAndRender({ screen, listState, contentEl, navigate }) { try { const feed = await authService.listSubscriptionsFeed(state.session.login, 200); + void ensureChannelReadBaselines(feed); let diaryPayload = null; try { diaryPayload = await authService.getPersonalDiary(state.session.login, 200, 'asc'); diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index cb0e73da..c3554386 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -3030,6 +3030,15 @@ export class AuthService { return response.payload || {}; } + async listUserSettings(login) { + const cleanLogin = String(login || '').trim(); + if (!cleanLogin) throw new Error('Не передан login'); + + const response = await this.ws.request('ListUserSettings', { login: cleanLogin }); + if (response.status !== 200) throw opError('ListUserSettings', response); + return response.payload || {}; + } + async upsertUserSetting({ login, settingType,