This commit is contained in:
rosetta
2026-01-30 05:01:05 +02:00
commit 83f38dc63f
327 changed files with 18725 additions and 0 deletions

View File

@@ -0,0 +1,836 @@
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<Message[]>) => void;
dialog: string;
clearDialogCache: () => void;
prepareAttachmentsToSend: (password: string, attachments: Attachment[]) => Promise<Attachment[]>;
loadMessagesToTop: () => Promise<void>;
loadMessagesToMessageId: (messageId: string) => Promise<void>;
}
export const DialogContext = createContext<DialogContextValue | null>(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<Message[]>([]);
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<NodeJS.Timeout>(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)){
/**
* Если сообщение не от меня и не групповое,
* расшифровываем ключ чачи своим приватным ключом
*/
console.info("Decrypting chacha key for message");
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<Attachment[]> => {
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<Attachment[]> => {
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 (
<DialogContext.Provider value={{
loading,
messages,
setMessages,
clearDialogCache: () => {
setDialogsCache(dialogsCache.filter((cache) => cache.publicKey != props.dialog));
},
dialog: props.dialog,
prepareAttachmentsToSend,
loadMessagesToTop,
loadMessagesToMessageId
}}>
{props.children}
</DialogContext.Provider>
)
}