SHA256
Improve direct message history and SQLite resilience
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user