SHA256
Исправить синхронизацию прочитанных каналов
This commit is contained in:
@@ -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('');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user