Compare commits

...
5 changed files with 73 additions and 17 deletions
@@ -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) {
+3 -2
View File
@@ -24,8 +24,9 @@
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
>
> `unreadCount` для канала считается по `user_settings`:
> если для пары `ownerBlockchainName/channelName` ещё нет записи, канал считается полностью прочитанным, а UI при первом открытии записывает текущий курсор чтения.
> `unreadCount` для канала считается по `user_settings`.
> Для собственных каналов владельцу всегда возвращается `unreadCount = 0`, чтобы его собственные публикации не становились «новыми» для него самого.
> Если для пары `ownerBlockchainName/channelName` ещё нет записи, канал временно считается полностью прочитанным; UI при загрузке списка каналов создаёт baseline на текущем `messagesCount`. После этого новые публикации увеличивают `unreadCount` до продвижения курсора чтения.
---
+3 -14
View File
@@ -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;
+54
View File
@@ -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');
+9
View File
@@ -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,