import { chacha20Decrypt, decodeWithPassword, decrypt, encodeWithPassword, generateMd5 } from '@/app/crypto/crypto'; import { useDatabase } from '@/app/providers/DatabaseProvider/useDatabase'; import { createContext, useEffect, useRef, useState } from 'react'; import { Attachment, AttachmentType, PacketMessage } from '@/app/providers/ProtocolProvider/protocol/packets/packet.message'; import { usePrivatePlain } from '../AccountProvider/usePrivatePlain'; import { usePublicKey } from '../AccountProvider/usePublicKey'; import { PacketRead } from '@/app/providers/ProtocolProvider/protocol/packets/packet.read'; import { usePrivateKeyHash } from '../AccountProvider/usePrivateKeyHash'; import { useMemory } from '../MemoryProvider/useMemory'; import { useBlacklist } from '../BlacklistProvider/useBlacklist'; import { useLogger } from '@/app/hooks/useLogger'; import { useSender } from '../ProtocolProvider/useSender'; import { usePacket } from '../ProtocolProvider/usePacket'; import { MAX_MESSAGES_LOAD, MESSAGE_MAX_LOADED, MESSAGE_MAX_TIME_TO_DELEVERED_S, TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD } from '@/app/constants'; import { PacketDelivery } from '@/app/providers/ProtocolProvider/protocol/packets/packet.delivery'; import { useIdle } from '@mantine/hooks'; import { useWindowFocus } from '@/app/hooks/useWindowFocus'; import { useDialogsCache } from './useDialogsCache'; import { useConsoleLogger } from '@/app/hooks/useConsoleLogger'; import { useViewPanelsState, ViewPanelsState } from '@/app/hooks/useViewPanelsState'; import { generateRandomKeyFormSeed } from '@/app/utils/utils'; import { MessageReply } from './useReplyMessages'; import { useTransport } from '../TransportProvider/useTransport'; import { useFileStorage } from '@/app/hooks/useFileStorage'; import { useSystemAccounts } from '../SystemAccountsProvider/useSystemAccounts'; import { useDialogsList } from '../DialogListProvider/useDialogsList'; import { useGroups } from './useGroups'; import { useMentions } from '../DialogStateProvider.tsx/useMentions'; export interface DialogContextValue { loading: boolean; messages: Message[]; setMessages: (messages: React.SetStateAction) => void; dialog: string; clearDialogCache: () => void; prepareAttachmentsToSend: (password: string, attachments: Attachment[]) => Promise; loadMessagesToTop: () => Promise; loadMessagesToMessageId: (messageId: string) => Promise; } export const DialogContext = createContext(null); export enum DeliveredMessageState { WAITING, DELIVERED, ERROR } export interface AttachmentMeta { id: string; type: AttachmentType; preview: string; } export interface Message { from_public_key: string; to_public_key: string; content: string; timestamp: number; readed: number; chacha_key: string; from_me: number; plain_message: string; delivered: DeliveredMessageState; message_id: string; attachments: Attachment[]; } interface DialogProviderProps { children: React.ReactNode; dialog: string; } export function DialogProvider(props: DialogProviderProps) { const [messages, setMessages] = useState([]); const {allQuery, runQuery} = useDatabase(); const privatePlain = usePrivatePlain(); const publicKey = usePublicKey(); const privateKey = usePrivateKeyHash(); const send = useSender(); const [__, setCurrentDialogPublicKeyView] = useMemory("current-dialog-public-key-view", "", true); const log = useLogger('DialogProvider'); const [blocked] = useBlacklist(props.dialog) const lastMessageTimeRef = useRef(0); const idle = useIdle(TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD * 1000); const focus = useWindowFocus(); const [loading, setLoading] = useState(false); const {getDialogCache, addOrUpdateDialogCache, dialogsCache, setDialogsCache} = useDialogsCache(); const {info, warn, error} = useConsoleLogger('DialogProvider'); const [viewState] = useViewPanelsState(); const {uploadFile} = useTransport(); const {readFile} = useFileStorage(); const intervalsRef = useRef(null); const systemAccounts = useSystemAccounts(); const {updateDialog} = useDialogsList(); const {hasGroup, getGroupKey} = useGroups(); const {popMention, isMentioned} = useMentions(); useEffect(() => { setCurrentDialogPublicKeyView(props.dialog); return () => { setCurrentDialogPublicKeyView(""); } }, [props.dialog]); useEffect(() => { if(props.dialog == "demo"){ return; } if(idle){ return; } (async () => { if(hasGroup(props.dialog)){ await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND account = ? AND read != 1 AND from_public_key != ?`, [props.dialog, publicKey, publicKey]); }else{ await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]); } updateDialog(props.dialog); })(); }, [idle, props.dialog]); useEffect(() => { if(props.dialog == "demo"){ return; } setMessages([]); if(props.dialog == "" || privatePlain == "") { return; } (async () => { let dialogCacheEntry = getDialogCache(props.dialog); if(dialogCacheEntry.length > 0){ const messagesToLoadFromCache = dialogCacheEntry.slice(-MESSAGE_MAX_LOADED); setMessages(messagesToLoadFromCache); info("Loading messages for " + props.dialog + " from cache"); setLoading(false); if(hasGroup(props.dialog)){ await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND account = ? AND read != 1 AND from_public_key != ?`, [props.dialog, publicKey, publicKey]); }else{ await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]); } if(isMentioned(props.dialog)){ /** * Удаляем упоминания потому что мы только что загрузили * диалог из кэша, может быть в нем есть упоминания */ for(let i = 0; i < messagesToLoadFromCache.length; i++){ const message = messagesToLoadFromCache[i]; popMention({ dialog_id: props.dialog, message_id: message.message_id }); } } updateDialog(props.dialog); return; } info("Loading messages for " + props.dialog + " from database"); setLoading(true); let result: any[] = []; if (props.dialog != publicKey) { if(hasGroup(props.dialog)){ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [props.dialog, publicKey, MAX_MESSAGES_LOAD]); }else{ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [props.dialog, props.dialog, publicKey, publicKey, publicKey, MAX_MESSAGES_LOAD]); } } else { result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [publicKey, publicKey, publicKey, MAX_MESSAGES_LOAD]); } await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]); const finalMessages : Message[] = []; let readUpdated = false; for(let i = 0; i < result.length; i++){ const message = result[i]; if(message.read != 1 && !readUpdated){ readUpdated = true; } let decryptKey = ''; if(message.from_me){ /** * Если сообщение от меня, то ключ расшифровки для вложений * не нужен, передаем пустую строку, так как под капотом * в MessageAttachment.tsx при расшифровке вложений используется * локальный ключ, а не тот что в сообщении, так как файл и так находится * у нас локально */ decryptKey = ''; } if(hasGroup(props.dialog)){ /** * Если это групповое сообщение, то получаем ключ группы */ decryptKey = await getGroupKey(props.dialog); } if(!message.from_me && !hasGroup(props.dialog)){ /** * Если сообщение не от меня и не групповое, * расшифровываем ключ чачи своим приватным ключом */ decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8'); } finalMessages.push({ from_public_key: message.from_public_key, to_public_key: message.to_public_key, content: "__ENCRYPTED__", timestamp: message.timestamp, readed: message.read || message.from_public_key == message.to_public_key, chacha_key: decryptKey, from_me: message.from_me, plain_message: await loadMessage(message.plain_message), delivered: message.delivered, message_id: message.message_id, attachments: await loadAttachments(message.attachments) }); if(isMentioned(props.dialog)){ /** * Если мы были упомянуты в этом диалоге, то убираем упоминание, * так как мы только что загрузили это сообщение */ popMention({ dialog_id: props.dialog, message_id: message.message_id }); } } if(readUpdated){ updateDialog(props.dialog); } setMessages(finalMessages); setLoading(false); })(); }, [props.dialog]); useEffect(() => { if(props.dialog == "demo"){ return; } if(!messages || messages.length == 0){ return; } addOrUpdateDialogCache(props.dialog, messages); }, [props.dialog, messages]) useEffect(() => { if(props.dialog == publicKey || messages.length == 0 || blocked || idle || lastMessageTimeRef.current == messages[messages.length - 1].timestamp || !focus){ return; } if(viewState == ViewPanelsState.DIALOGS_PANEL_ONLY){ /** * Если мы сейчас видим только диалоги * то сообщение мы не читаем */ return; } if(systemAccounts.find(acc => acc.publicKey == props.dialog)){ /** * Системные аккаунты не отмечаем как прочитанные */ return; } const readPacket = new PacketRead(); readPacket.setFromPublicKey(publicKey); readPacket.setToPublicKey(props.dialog); readPacket.setPrivateKey(privateKey); send(readPacket); log("Send read packet to " + props.dialog); info("Send read packet"); lastMessageTimeRef.current = messages[messages.length - 1].timestamp; }, [props.dialog, viewState, focus, messages, blocked, idle]); usePacket(0x07, async (packet : PacketRead) => { info("Read packet received in dialog provider"); const fromPublicKey = packet.getFromPublicKey(); if(hasGroup(props.dialog)){ /** * Для групп обработка чтения есть ниже */ return; } if(fromPublicKey != props.dialog && !idle){ return; } setMessages((prev) => prev.map((msg) => { if(msg.from_public_key == publicKey && !msg.readed){ return { ...msg, readed: 1 } } return msg; })); //updateDialog(props.dialog); }, [idle, props.dialog]); usePacket(0x07, async (packet : PacketRead) => { info("Read packet received in dialog provider"); //const fromPublicKey = packet.getFromPublicKey(); const toPublicKey = packet.getToPublicKey(); if(!hasGroup(props.dialog)){ /** * Для личных сообщений обработка чтения выше */ return; } if(toPublicKey != props.dialog && !idle){ return; } setMessages((prev) => prev.map((msg) => { if(msg.from_public_key == publicKey && !msg.readed){ return { ...msg, readed: 1 } } return msg; })); //updateDialog(props.dialog); }, [idle, props.dialog]); usePacket(0x08, async (packet : PacketDelivery) => { info("Delivery packet received in dialog provider"); const fromPublicKey = packet.getToPublicKey(); const messageId = packet.getMessageId(); if(fromPublicKey != props.dialog){ return; } setMessages((prev) => prev.map((msg) => { if(msg.message_id == messageId && msg.delivered != DeliveredMessageState.DELIVERED){ return { ...msg, delivered: DeliveredMessageState.DELIVERED, timestamp: Date.now() } } return msg; })); }, [props.dialog]); /** * Обработчик для личных сообщений */ usePacket(0x06, async (packet: PacketMessage) => { const fromPublicKey = packet.getFromPublicKey(); const toPublicKey = packet.getToPublicKey(); if(hasGroup(props.dialog)){ /** * Если это групповое сообщение, то для него есть * другой обработчик ниже */ return; } if(fromPublicKey != props.dialog || toPublicKey != publicKey){ console.info("From " + fromPublicKey + " to " + props.dialog + " ignore"); return; } if(blocked){ warn("Message from blocked user, ignore " + fromPublicKey); log("Message from blocked user, ignore " + fromPublicKey); return; } const content = packet.getContent(); const chachaKey = packet.getChachaKey(); const timestamp = packet.getTimestamp(); /** * Генерация рандомного ID сообщения по SEED нужна для того, * чтобы сообщение записанное здесь в стек сообщений совпадало * с тем что записывается в БД в файле useDialogFiber.ts */ const messageId = generateRandomKeyFormSeed(16, fromPublicKey + toPublicKey + timestamp.toString()); const chachaDecryptedKey = Buffer.from( await decrypt(chachaKey, privatePlain), "binary"); const key = chachaDecryptedKey.slice(0, 32); const nonce = chachaDecryptedKey.slice(32); const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); let attachments: Attachment[] = []; for(let i = 0; i < packet.getAttachments().length; i++) { const attachment = packet.getAttachments()[i]; attachments.push({ id: attachment.id, preview: attachment.preview, type: attachment.type, blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob) : "" }); } const newMessage : Message = { from_public_key: fromPublicKey, to_public_key: toPublicKey, content: content, timestamp: timestamp, readed: idle ? 0 : 1, chacha_key: chachaDecryptedKey.toString('utf-8'), from_me: fromPublicKey == publicKey ? 1 : 0, plain_message: (decryptedContent as string), delivered: DeliveredMessageState.DELIVERED, message_id: messageId, attachments: attachments }; setMessages((prev) => ([...prev, newMessage])); }, [blocked, messages, idle, props.dialog]); /** * Обработчик для групповых сообщений */ usePacket(0x06, async (packet: PacketMessage) => { const fromPublicKey = packet.getFromPublicKey(); const toPublicKey = packet.getToPublicKey(); if(toPublicKey != props.dialog){ /** * Исправление кросс диалогового сообщения */ return; } if(!hasGroup(props.dialog)){ /** * Если это не групповое сообщение, то для него есть * другой обработчик выше */ return; } const content = packet.getContent(); const timestamp = packet.getTimestamp(); /** * Генерация рандомного ID сообщения по SEED нужна для того, * чтобы сообщение записанное здесь в стек сообщений совпадало * с тем что записывается в БД в файле useDialogFiber.ts */ const messageId = generateRandomKeyFormSeed(16, fromPublicKey + toPublicKey + timestamp.toString()); const groupKey = await getGroupKey(toPublicKey); if(!groupKey){ log("Group key not found for group " + toPublicKey); error("Message dropped because group key not found for group " + toPublicKey); return; } info("New group message packet received from " + fromPublicKey); let decryptedContent = ''; try{ decryptedContent = await decodeWithPassword(groupKey, content); }catch(e) { decryptedContent = ''; } let attachments: Attachment[] = []; for(let i = 0; i < packet.getAttachments().length; i++) { const attachment = packet.getAttachments()[i]; attachments.push({ id: attachment.id, preview: attachment.preview, type: attachment.type, blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : "" }); } const newMessage : Message = { from_public_key: fromPublicKey, to_public_key: toPublicKey, content: content, timestamp: timestamp, readed: idle ? 0 : 1, chacha_key: groupKey, from_me: fromPublicKey == publicKey ? 1 : 0, plain_message: decryptedContent, delivered: DeliveredMessageState.DELIVERED, message_id: messageId, attachments: attachments }; setMessages((prev) => ([...prev, newMessage])); }, [messages, idle, props.dialog]); /** * Расшифровывает сообщение * @param message Зашифрованное сообщение * @returns Расшифрованное сообщение */ const loadMessage = async (message : string) => { try{ return await decodeWithPassword(privatePlain, message); }catch(e){ return ""; } } /** * Загружает часть диалога где есть определенный message_id */ const loadMessagesToMessageId = async (messageId: string) => { warn("Load messages to message ID " + messageId + " for " + props.dialog); if(props.dialog == "DELETED" || privatePlain == "") { return; } let result : any[] = []; if(props.dialog != publicKey){ if(hasGroup(props.dialog)){ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [props.dialog, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]); }else{ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [props.dialog, props.dialog, publicKey, publicKey, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]); } }else{ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [publicKey, publicKey, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]); } const finalMessages : Message[] = []; for(let i = 0; i < result.length; i++){ const message = result[i]; let decryptKey = ''; if(message.from_me){ /** * Если сообщение от меня, то ключ расшифровки для вложений * не нужен, передаем пустую строку, так как под капотом * в MessageAttachment.tsx при расшифровке вложений используется * локальный ключ, а не тот что в сообщении, так как файл и так находится * у нас локально */ decryptKey = ''; } if(hasGroup(props.dialog)){ /** * Если это групповое сообщение, то получаем ключ группы */ decryptKey = await getGroupKey(props.dialog); } if(!message.from_me && !hasGroup(props.dialog)){ /** * Если сообщение не от меня и не групповое, * расшифровываем ключ чачи своим приватным ключом */ decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8'); } finalMessages.push({ from_public_key: message.from_public_key, to_public_key: message.to_public_key, content: "__ENCRYPTED__", timestamp: message.timestamp, readed: message.read || message.from_public_key == message.to_public_key || !message.from_me, chacha_key: decryptKey, from_me: message.from_me, plain_message: await loadMessage(message.plain_message), delivered: message.delivered, message_id: message.message_id, attachments: await loadAttachments(message.attachments) }); if(isMentioned(props.dialog)){ /** * Если мы были упомянуты в этом диалоге, то убираем упоминание, * так как мы только что загрузили это сообщение */ popMention({ dialog_id: props.dialog, message_id: message.message_id }); } } if(finalMessages.length == 0) { return; } setMessages([...finalMessages]); } /** * Загружает сообщения в верх диалога, когда пользователь * скроллит вверх и доскроллил до конца * @returns */ const loadMessagesToTop = async () => { warn("Load messages to top for " + props.dialog); if(props.dialog == "DELETED" || privatePlain == "") { return; } let result : any[] = []; if(props.dialog != publicKey){ if(hasGroup(props.dialog)){ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [props.dialog, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]); }else{ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [props.dialog, props.dialog, publicKey, publicKey, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]); } }else{ result = await allQuery(` SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC `, [publicKey, publicKey, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]); } const finalMessages : Message[] = []; for(let i = 0; i < result.length; i++){ const message = result[i]; let decryptKey = ''; if(message.from_me){ /** * Если сообщение от меня, то ключ расшифровки для вложений * не нужен, передаем пустую строку, так как под капотом * в MessageAttachment.tsx при расшифровке вложений используется * локальный ключ, а не тот что в сообщении, так как файл и так находится * у нас локально */ decryptKey = ''; } if(hasGroup(props.dialog)){ /** * Если это групповое сообщение, то получаем ключ группы */ decryptKey = await getGroupKey(props.dialog); } if(!message.from_me && !hasGroup(props.dialog)){ /** * Если сообщение не от меня и не групповое, * расшифровываем ключ чачи своим приватным ключом */ decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8'); } finalMessages.push({ from_public_key: message.from_public_key, to_public_key: message.to_public_key, content: "__ENCRYPTED__", timestamp: message.timestamp, readed: message.read || message.from_public_key == message.to_public_key || !message.from_me, chacha_key: decryptKey, from_me: message.from_me, plain_message: await loadMessage(message.plain_message), delivered: message.delivered, message_id: message.message_id, attachments: await loadAttachments(message.attachments) }); if(isMentioned(props.dialog)){ /** * Если мы были упомянуты в этом диалоге, то убираем упоминание, * так как мы только что загрузили это сообщение */ popMention({ dialog_id: props.dialog, message_id: message.message_id }); } } if(finalMessages.length == 0) { return; } setMessages([...finalMessages, ...messages]); } /** * Загружает вложения из JSON строки. * Если вложение не было загружено (то есть его нет на диске), * то в blob кладется пустая строка, далее в useAttachment * это отработается (загрузится превью вложение и появится возможность скачать и тд) * @param jsonAttachments JSON вложений AttachmentMeta формат * @returns Вложения */ const loadAttachments = async (jsonAttachments : string) : Promise => { if(jsonAttachments == "[]") { return []; } try { const attachmentsMeta : AttachmentMeta[] = JSON.parse(jsonAttachments); const attachments : Attachment[] = []; for(const meta of attachmentsMeta) { if(meta.type == AttachmentType.FILE){ /** * Все кроме файлов декодируем заранее */ attachments.push({ ...meta, blob: "" }); continue; } const fileData = await readFile(`m/${await generateMd5(meta.id + publicKey)}`); if(!fileData) { attachments.push({ ...meta, blob: "" }); continue; } const decrypted = await decodeWithPassword(privatePlain, Buffer.from(fileData, 'binary').toString()); attachments.push({ id: meta.id, blob: decrypted, type: meta.type, preview: meta.preview }); } return attachments; }catch(e) { error("Failed to parse attachments"); } return []; } /** * Обновляет временную метку в сообщении, пока вложения отправляются, * потому что если этого не делать, то сообщение может быть помечено как * не доставленное из-за таймаута доставки * @param attachments Вложения */ const doTimestampUpdateImMessageWhileAttachmentsSend = (attachments : Attachment[]) => { if(intervalsRef.current){ clearInterval(intervalsRef.current); } intervalsRef.current = setInterval(() => { //update timestamp in message to keep message marked as error updateDialog(props.dialog); setMessages((prev) => { return prev.map((value) => { if(value.attachments.length <= 0){ return value; } if(value.attachments[0].id != attachments[0].id){ return value; } runQuery("UPDATE messages SET timestamp = ? WHERE message_id = ?", [Date.now(), value.message_id]); return { ...value, timestamp: Date.now() }; }) }); }, (MESSAGE_MAX_TIME_TO_DELEVERED_S / 2) * 1000); } /** * Удаляет старый тег если вложения были подготовлены заново * например при пересылке сообщений */ const removeOldTagIfAttachemtnsRePreapred = (preview : string) => { if(preview.indexOf("::") == -1){ return preview; } let parts = preview.split("::"); return parts.slice(1).join("::"); } /** * Подготавливает вложения для отправки. Подготовка * состоит в загрузке файлов на транспортный сервер, мы не делаем * это через WebSocket из-за ограничений по размеру сообщений, * а так же из-за надежности доставки файлов через HTTP * @param attachments Attachments to prepare for sending */ const prepareAttachmentsToSend = async (password: string, attachments : Attachment[], rePrepared : boolean = false) : Promise => { if(attachments.length <= 0){ return []; } let prepared : Attachment[] = []; try{ for(let i = 0; i < attachments.length; i++){ const attachment : Attachment = attachments[i]; if(attachment.type == AttachmentType.MESSAGES){ let reply : MessageReply[] = JSON.parse(attachment.blob) for(let j = 0; j < reply.length; j++){ reply[j].attachments = await prepareAttachmentsToSend(password, reply[j].attachments, true); } prepared.push({ ...attachment, blob: await encodeWithPassword(password, JSON.stringify(reply)) }); continue; } doTimestampUpdateImMessageWhileAttachmentsSend(attachments); const content = await encodeWithPassword(password, attachment.blob); const upid = attachment.id; info(`Uploading attachment with upid: ${upid}`); info(`Attachment content length: ${content.length}`); let tag = await uploadFile(upid, content); info(`Uploaded attachment with upid: ${upid}, received tag: ${tag}`); if(intervalsRef.current != null){ clearInterval(intervalsRef.current); } prepared.push({ ...attachment, preview: tag + "::" + (rePrepared ? removeOldTagIfAttachemtnsRePreapred(attachment.preview) : attachment.preview), blob: "" }); } return prepared; }catch(e){ return prepared; } } return ( { setDialogsCache(dialogsCache.filter((cache) => cache.publicKey != props.dialog)); }, dialog: props.dialog, prepareAttachmentsToSend, loadMessagesToTop, loadMessagesToMessageId }}> {props.children} ) }