Improve direct message history and SQLite resilience

This commit is contained in:
AidarKC
2026-07-22 13:09:42 +04:00
parent 2f3b1571e5
commit 93673e5786
18 changed files with 908 additions and 233 deletions
+131 -3
View File
@@ -144,6 +144,7 @@ function openMessageActionsMenu({
anchorX = 0,
anchorY = 0,
messageText = '',
infoText = '',
canReply = false,
showReadAloud = true,
canEdit = false,
@@ -165,6 +166,7 @@ function openMessageActionsMenu({
${showReadAloud ? '<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-read">Прочесть</button>' : ''}
${canEdit ? '<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-edit">Изменить</button>' : ''}
${canDelete ? '<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="msg-action-delete">Удалить</button>' : ''}
${String(infoText || '').trim() ? `<div class="meta-muted">${String(infoText || '').trim()}</div>` : ''}
</div>
</div>
`;
@@ -438,9 +440,27 @@ function buildCallStatusText(callSummary) {
return 'Не дозвонился';
}
function resolveDeliveryStatus(msg) {
function resolveEffectiveReadState(messages, msg) {
if (msg?.from !== 'out') return { isRead: false, readAtMs: 0 };
const explicitReadAtMs = Number(msg?.readAtMs || 0);
if (Number.isFinite(explicitReadAtMs) && explicitReadAtMs > 0) {
return { isRead: true, readAtMs: explicitReadAtMs };
}
if (msg?.secondTick) {
return { isRead: true, readAtMs: 0 };
}
const messageTimeMs = resolveMessageTimeMs(msg);
const hasNewerRead = (messages || []).some((row) => {
if (row?.from !== 'out') return false;
if (resolveMessageTimeMs(row) <= messageTimeMs) return false;
return Boolean(Number(row?.readAtMs || 0) > 0 || row?.secondTick);
});
return { isRead: hasNewerRead, readAtMs: 0 };
}
function resolveDeliveryStatus(messages, msg) {
if (msg?.from !== 'out') return '';
if (msg?.secondTick) return '✓✓';
if (resolveEffectiveReadState(messages, msg).isRead) return '✓✓';
if (msg?.firstTick) return '✓';
return '…';
}
@@ -625,7 +645,7 @@ function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode
timeNode.textContent = formatMessageTime(resolveMessageTimeMs(msg));
metaNode.append(timeNode);
const status = resolveDeliveryStatus(msg);
const status = resolveDeliveryStatus(messages, msg);
if (status) {
const statusNode = document.createElement('span');
statusNode.className = 'bubble-status';
@@ -684,6 +704,49 @@ function preserveComposerSelection(input, callback) {
}
}
async function mergeDirectMessagesPage(chatId, payloadMessages) {
const items = Array.isArray(payloadMessages) ? [...payloadMessages] : [];
items.sort((a, b) => Number(a?.timeMs || 0) - Number(b?.timeMs || 0));
for (const item of items) {
const blobB64 = String(item?.blobB64 || '').trim();
const messageKey = String(item?.messageKey || '').trim();
if (!blobB64 || !messageKey) continue;
try {
const parsed = authService.parseSignedMessageBlob(blobB64);
let text = '';
try {
const decrypted = await authService.decryptSignedMessageContent({
parsed,
login: state.session.login,
storagePwd: state.session.storagePwdInMemory,
});
text = String(decrypted?.text || '');
} catch (error) {
text = `Нерасшифрованное сообщение (${error?.message || 'decrypt failed'})`;
}
addSignedMessageToChat({
chatId,
messageKey,
baseKey: String(item?.baseKey || parsed?.baseKey || ''),
from: Number(parsed?.messageType || 0) === 2 ? 'out' : 'in',
text,
messageType: Number(parsed?.messageType || item?.messageType || 0),
unread: false,
readAtMs: Number(item?.readAtMs || 0),
rawBlobB64: blobB64,
revisionTimeMs: Number(item?.revisionTimeMs || parsed?.revisionTimeMs || 0),
});
} catch (error) {
addAppLogEntry({
level: 'warn',
source: 'dm-history',
message: 'Не удалось обработать сообщение из истории DM',
details: { chatId, messageKey, error: error?.message || 'unknown' },
});
}
}
}
function setChatKeyboardOpen(isOpen) {
document.body.classList.toggle('chat-keyboard-open', !!isOpen);
}
@@ -703,6 +766,12 @@ export function render({ navigate, route }) {
const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings);
const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase());
const hasUnreadIncoming = getChatMessages(chatId).some((msg) => msg?.from === 'in' && msg?.unread);
let historyHasMore = true;
let historyLoading = false;
let historyNextBeforeTimeMs = 0;
let historyNextBeforeMessageKey = '';
let historyBootstrapped = false;
let boundScrollContainer = null;
const handleReadAloud = async (msg) => {
if (!isTextToSpeechConfigured(state.entrySettings)) {
@@ -1195,10 +1264,14 @@ export function render({ navigate, route }) {
const handleOpenActions = (msg, event) => {
const parsed = parseDmTechBlocks(String(msg?.text || ''));
const readState = resolveEffectiveReadState(getChatMessages(chatId), msg);
openMessageActionsMenu({
anchorX: Number(event?.clientX || 0),
anchorY: Number(event?.clientY || 0),
messageText: parsed?.displayText || msg?.text || '',
infoText: (msg?.from === 'out' && readState.readAtMs > 0)
? `Прочитано: ${formatMessageTime(readState.readAtMs)}`
: '',
canReply: true,
showReadAloud: isTextToSpeechReady,
canEdit: msg?.from === 'out' && Number(msg?.messageType || 0) === 2 && !parsed?.callSummary,
@@ -1227,6 +1300,55 @@ export function render({ navigate, route }) {
});
};
const loadHistoryPage = async ({ preserveScroll = false } = {}) => {
if (historyLoading) return;
if (historyBootstrapped && !historyHasMore) return;
historyLoading = true;
const scrollContainer = boundScrollContainer || log.closest('.screen-content') || wrap.parentElement || wrap;
const prevScrollTop = Number(scrollContainer?.scrollTop || 0);
const prevScrollHeight = Number(scrollContainer?.scrollHeight || 0);
try {
const payload = await authService.getDirectMessages({
peerLogin: chatId,
limit: 50,
beforeTimeMs: historyBootstrapped ? historyNextBeforeTimeMs : 0,
beforeMessageKey: historyBootstrapped ? historyNextBeforeMessageKey : '',
});
await mergeDirectMessagesPage(chatId, payload?.messages || []);
historyHasMore = Boolean(payload?.hasMore);
historyNextBeforeTimeMs = Number(payload?.nextBeforeTimeMs || 0);
historyNextBeforeMessageKey = String(payload?.nextBeforeMessageKey || '').trim();
historyBootstrapped = true;
renderLog(log, chatId, { onOpenActions: handleOpenActions, markAsRead: false, scrollMode: preserveScroll ? 'preserve' : 'latest' });
if (preserveScroll) {
window.requestAnimationFrame(() => {
const nextHeight = Number(scrollContainer?.scrollHeight || 0);
scrollContainer.scrollTop = Math.max(0, prevScrollTop + (nextHeight - prevScrollHeight));
});
} else {
window.requestAnimationFrame(() => scrollToLatestMessage(log));
}
void sendReadReceiptsForVisible(chatId);
} catch (error) {
addAppLogEntry({
level: 'warn',
source: 'dm-history',
message: 'Не удалось загрузить историю DM',
details: { chatId, error: error?.message || 'unknown' },
});
} finally {
historyLoading = false;
}
};
const handleHistoryScroll = () => {
const scrollContainer = boundScrollContainer || log.closest('.screen-content') || wrap.parentElement || wrap;
if (!scrollContainer) return;
if (Number(scrollContainer.scrollTop || 0) > 120) return;
if (!historyBootstrapped || !historyHasMore || historyLoading) return;
void loadHistoryPage({ preserveScroll: true });
};
editCancelBtn?.addEventListener('click', () => {
if (activeEdit) {
cancelEditMode({ restoreDraft: true });
@@ -1367,12 +1489,18 @@ export function render({ navigate, route }) {
}
}, 220);
void sendReadReceiptsForVisible(chatId);
window.requestAnimationFrame(() => {
boundScrollContainer = log.closest('.screen-content') || wrap.parentElement || wrap;
boundScrollContainer?.addEventListener('scroll', handleHistoryScroll, { passive: true });
void loadHistoryPage({ preserveScroll: false });
});
screen.cleanup = () => {
setChatKeyboardOpen(false);
stopAllTwemojiAnimations();
window.removeEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
window.visualViewport?.removeEventListener('resize', syncKeyboardUi);
window.removeEventListener('resize', syncKeyboardUi);
boundScrollContainer?.removeEventListener('scroll', handleHistoryScroll);
};
return screen;
}