diff --git a/app/components/MessageAttachments/MessageAttachments.tsx b/app/components/MessageAttachments/MessageAttachments.tsx index 44c7714..2cef177 100644 --- a/app/components/MessageAttachments/MessageAttachments.tsx +++ b/app/components/MessageAttachments/MessageAttachments.tsx @@ -8,6 +8,7 @@ import { ErrorBoundaryProvider } from "@/app/providers/ErrorBoundaryProvider/Err import { AttachmentError } from "../AttachmentError/AttachmentError"; import { MessageAvatar } from "./MessageAvatar"; import { MessageProps } from "../Messages/Message"; +import { MessageCall } from "./MessageCall"; export interface MessageAttachmentsProps { attachments: Attachment[]; @@ -51,6 +52,8 @@ export function MessageAttachments(props: MessageAttachmentsProps) { return case AttachmentType.AVATAR: return + case AttachmentType.CALL: + return default: return ; } diff --git a/app/components/MessageAttachments/MessageCall.tsx b/app/components/MessageAttachments/MessageCall.tsx new file mode 100644 index 0000000..478afdd --- /dev/null +++ b/app/components/MessageAttachments/MessageCall.tsx @@ -0,0 +1,62 @@ +import { useAttachment } from "@/app/providers/AttachmentProvider/useAttachment"; +import { AttachmentProps } from "./MessageAttachments"; +import { Avatar, Box, Flex, Text } from "@mantine/core"; +import { useRosettaColors } from "@/app/hooks/useRosettaColors"; +import { IconPhoneOutgoing, IconX } from "@tabler/icons-react"; +import { translateDurationToTime } from "@/app/providers/CallProvider/translateDurationTime"; + +export function MessageCall(props: AttachmentProps) { + const { + getPreview, + } = + useAttachment( + props.attachment, + props.parent, + ); + const preview = getPreview(); + const callerRole = preview.split("::")[0]; + const duration = parseInt(preview.split("::")[1]); + const colors = useRosettaColors(); + const error = duration == 0; + + return ( + + + + {!error && <> + {callerRole == "0" && ( + + )} + {callerRole == "1" && ( + + )} + } + {error && <> + + } + + + { + error ? (callerRole == "0" ? "Missed call" : "Rejected call") : (callerRole == "0" ? "Incoming call" : "Outgoing call") + } + {!error && + + {translateDurationToTime(duration)} + + } + {error && + Call was not answered or was rejected + } + + + + ); +} \ No newline at end of file diff --git a/app/providers/AttachmentProvider/usePrepareAttachment.ts b/app/providers/AttachmentProvider/usePrepareAttachment.ts new file mode 100644 index 0000000..dc940bd --- /dev/null +++ b/app/providers/AttachmentProvider/usePrepareAttachment.ts @@ -0,0 +1,148 @@ +import { encodeWithPassword } from "@/app/workers/crypto/crypto"; +import { MessageReply } from "../DialogProvider/useReplyMessages"; +import { Attachment, AttachmentType } from "../ProtocolProvider/protocol/packets/packet.message"; +import { base64ImageToBlurhash } from "@/app/workers/image/image"; +import { MESSAGE_MAX_TIME_TO_DELEVERED_S } from "@/app/constants"; +import { useContext, useRef } from "react"; +import { useTransport } from "../TransportProvider/useTransport"; +import { useDialogsList } from "../DialogListProvider/useDialogsList"; +import { useDatabase } from "../DatabaseProvider/useDatabase"; +import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; +import { useDialogsCache } from "../DialogProvider/useDialogsCache"; +import { DialogContext } from "../DialogProvider/DialogProvider"; + +export function usePrepareAttachment() { + const intervalsRef = useRef(null); + const {uploadFile} = useTransport(); + const {updateDialog} = useDialogsList(); + const {runQuery} = useDatabase(); + const {info} = useConsoleLogger('usePrepareAttachment'); + const {getDialogCache} = useDialogsCache(); + const context = useContext(DialogContext); + + const updateTimestampInDialogCache = (dialog : string, message_id: string) => { + const dialogCache = getDialogCache(dialog); + if(dialogCache == null){ + return; + } + for(let i = 0; i < dialogCache.length; i++){ + if(dialogCache[i].message_id == message_id){ + dialogCache[i].timestamp = Date.now(); + break; + } + } + } + + /** + * Обновляет временную метку в сообщении, пока вложения отправляются, + * потому что если этого не делать, то сообщение может быть помечено как + * не доставленное из-за таймаута доставки + * @param attachments Вложения + */ + const doTimestampUpdateImMessageWhileAttachmentsSend = (message_id: string, dialog: string) => { + if(intervalsRef.current){ + clearInterval(intervalsRef.current); + } + intervalsRef.current = setInterval(async () => { + /** + * Обновляем время в левом меню + */ + await runQuery("UPDATE messages SET timestamp = ? WHERE message_id = ?", [Date.now(), message_id]); + updateDialog(dialog); + /** + * Обновляем состояние в кэше диалогов + */ + updateTimestampInDialogCache(dialog, message_id); + + if(context == null || !context){ + /** + * Если этот диалог сейчас не открыт + */ + return; + } + context.setMessages((prev) => { + return prev.map((value) => { + if(value.message_id != message_id){ + return value; + } + 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 (message_id: string, dialog: string, 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(message_id, dialog, password, reply[j].attachments, true); + } + prepared.push({ + ...attachment, + blob: await encodeWithPassword(password, JSON.stringify(reply)) + }); + continue; + } + if((attachment.type == AttachmentType.IMAGE + || attachment.type == AttachmentType.AVATAR) && attachment.preview == ""){ + /** + * Загружаем превью blurhash для изображения + */ + const blurhash = await base64ImageToBlurhash(attachment.blob); + attachment.preview = blurhash; + } + doTimestampUpdateImMessageWhileAttachmentsSend(message_id, dialog); + 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 { + prepareAttachmentsToSend + } +} \ No newline at end of file diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 598a41d..c0afb4c 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -13,6 +13,9 @@ import { Button, Flex, Text } from "@mantine/core"; import { useSound } from "@/app/hooks/useSound"; import useWindow from "@/app/hooks/useWindow"; import { attachReceiverE2EE, attachSenderE2EE } from "./audioE2EE"; +import { useDeattachedSender } from "../DialogProvider/useDeattachedSender"; +import { AttachmentType } from "../ProtocolProvider/protocol/packets/packet.message"; +import { generateRandomKey } from "@/app/utils/utils"; export interface CallContextValue { call: (callable: string) => void; @@ -80,6 +83,7 @@ export function CallProvider(props : CallProviderProps) { const iceCandidatesBufferRef = useRef([]); const mutedRef = useRef(false); const soundRef = useRef(true); + const {sendMessage} = useDeattachedSender(); const {playSound, stopSound, stopLoopSound} = useSound(); const {setWindowPriority} = useWindow(); @@ -434,6 +438,7 @@ export function CallProvider(props : CallProviderProps) { remoteAudioRef.current.pause(); remoteAudioRef.current.srcObject = null; } + generateCallAttachment(); setDuration(0); durationIntervalRef.current && clearInterval(durationIntervalRef.current); setWindowPriority(false); @@ -453,6 +458,27 @@ export function CallProvider(props : CallProviderProps) { roleRef.current = null; } + /** + * Отправляет сообщение в диалог с звонящим с информацией о звонке + */ + const generateCallAttachment = () => { + let preview = ""; + if(roleRef.current == CallRole.CALLER){ + preview += "1::"; + } + if(roleRef.current == CallRole.CALLEE){ + preview += "0::"; + } + preview += duration.toString(); + + sendMessage(activeCall, "", [{ + id: generateRandomKey(16), + preview: preview, + type: AttachmentType.CALL, + blob: "" + }], false); + } + const accept = () => { if(callState != CallState.INCOMING){ /** diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index f255ea2..6d646fd 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -1,4 +1,4 @@ -import { chacha20Decrypt, decodeWithPassword, decrypt, encodeWithPassword, generateMd5 } from '@/app/workers/crypto/crypto'; +import { chacha20Decrypt, decodeWithPassword, decrypt, generateMd5 } from '@/app/workers/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'; @@ -11,21 +11,18 @@ 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 { MAX_MESSAGES_LOAD, MESSAGE_MAX_LOADED, 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 { 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'; -import { base64ImageToBlurhash } from '@/app/workers/image/image'; export interface DialogContextValue { loading: boolean; @@ -33,7 +30,6 @@ export interface DialogContextValue { setMessages: (messages: React.SetStateAction) => void; dialog: string; clearDialogCache: () => void; - prepareAttachmentsToSend: (password: string, attachments: Attachment[]) => Promise; loadMessagesToTop: () => Promise; loadMessagesToMessageId: (messageId: string) => Promise; } @@ -71,6 +67,23 @@ interface DialogProviderProps { dialog: string; } +type DialogMessageEvent = { + dialogId: string; + message: Message; +}; + +const bus = new EventTarget(); + +export const emitDialogMessage = (payload: DialogMessageEvent) => { + bus.dispatchEvent(new CustomEvent("dialog:message", { detail: payload })); +}; + +export const onDialogMessage = (handler: (payload: DialogMessageEvent) => void) => { + const listener = (e: Event) => handler((e as CustomEvent).detail); + bus.addEventListener("dialog:message", listener); + return () => bus.removeEventListener("dialog:message", listener); +}; + export function DialogProvider(props: DialogProviderProps) { const [messages, setMessages] = useState([]); const {allQuery, runQuery} = useDatabase(); @@ -88,15 +101,21 @@ export function DialogProvider(props: DialogProviderProps) { 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(() => { + const unsub = onDialogMessage(({ dialogId, message }) => { + if (dialogId !== props.dialog) return; + setMessages((prev) => [...prev, message]); + }); + return unsub; + }, [props.dialog]); + useEffect(() => { setCurrentDialogPublicKeyView(props.dialog); return () => { @@ -919,6 +938,16 @@ export function DialogProvider(props: DialogProviderProps) { }); continue; } + if(meta.type == AttachmentType.CALL){ + /** + * Если это звонок + */ + attachments.push({ + ...meta, + blob: "" + }); + continue; + } const fileData = await readFile(`m/${await generateMd5(meta.id + publicKey)}`); if(!fileData) { attachments.push({ @@ -940,110 +969,12 @@ export function DialogProvider(props: DialogProviderProps) { } return attachments; }catch(e) { + console.info(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; - } - if((attachment.type == AttachmentType.IMAGE - || attachment.type == AttachmentType.AVATAR) && attachment.preview == ""){ - /** - * Загружаем превью blurhash для изображения - */ - const blurhash = await base64ImageToBlurhash(attachment.blob); - attachment.preview = blurhash; - } - 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; - } - } - /** * Дедубликация сообщений по message_id, так как может возникать ситуация, что одно и то же сообщение * может загрузиться несколько раз при накладках сети, отставании часов, при синхронизации @@ -1071,7 +1002,6 @@ export function DialogProvider(props: DialogProviderProps) { setDialogsCache(dialogsCache.filter((cache) => cache.publicKey != props.dialog)); }, dialog: props.dialog, - prepareAttachmentsToSend, loadMessagesToTop, loadMessagesToMessageId }}> diff --git a/app/providers/DialogProvider/useDeattachedSender.ts b/app/providers/DialogProvider/useDeattachedSender.ts new file mode 100644 index 0000000..6074f3b --- /dev/null +++ b/app/providers/DialogProvider/useDeattachedSender.ts @@ -0,0 +1,160 @@ +import { generateRandomKey } from "@/app/utils/utils"; +import { Attachment, AttachmentType, PacketMessage } from "../ProtocolProvider/protocol/packets/packet.message"; +import { useGroups } from "./useGroups"; +import { chacha20Encrypt, encodeWithPassword, encrypt, generateMd5 } from "@/app/workers/crypto/crypto"; +import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; +import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; +import { AttachmentMeta, DeliveredMessageState, emitDialogMessage, Message } from "./DialogProvider"; +import { useDatabase } from "../DatabaseProvider/useDatabase"; +import { useFileStorage } from "@/app/hooks/useFileStorage"; +import { usePublicKey } from "../AccountProvider/usePublicKey"; +import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; +import { useDialogsList } from "../DialogListProvider/useDialogsList"; +import { useProtocolState } from "../ProtocolProvider/useProtocolState"; +import { usePrivateKeyHash } from "../AccountProvider/usePrivateKeyHash"; +import { useSender } from "../ProtocolProvider/useSender"; +import { usePrepareAttachment } from "../AttachmentProvider/usePrepareAttachment"; + +/** + * Используется для отправки сообщений не внутри DialogProvider, а например в CallProvider, + * когда нам нужно отправить сообщение от своего имени что мы совершли звонок (Attachment.CALL) + */ +export function useDeattachedSender() { + const {hasGroup, getGroupKey} = useGroups(); + const privatePlain = usePrivatePlain(); + const {warn} = useConsoleLogger('useDeattachedSender'); + const {runQuery} = useDatabase(); + const {writeFile} = useFileStorage(); + const publicKey = usePublicKey(); + const {updateDialog} = useDialogsList(); + const [protocolState] = useProtocolState(); + const privateKey = usePrivateKeyHash(); + const send = useSender(); + const {prepareAttachmentsToSend} = usePrepareAttachment(); + + /** + * Отправка сообщения в диалог + * @param dialog ID диалога, может быть как публичным ключом собеседника, так и ID группового диалога + * @param message Сообщение + * @param attachemnts Вложения + */ + const sendMessage = async (dialog: string, message: string, attachemnts : Attachment[], serverSent: boolean = false) => { + const messageId = generateRandomKey(16); + + + let cahchaEncrypted = {ciphertext: "", key: "", nonce: ""} as any; + let key = Buffer.from(""); + let encryptedKey = ""; + let plainMessage = ""; + let content = ""; + + if(!hasGroup(dialog)){ + cahchaEncrypted = (await chacha20Encrypt(message.trim()) as any); + key = Buffer.concat([ + Buffer.from(cahchaEncrypted.key, "hex"), + Buffer.from(cahchaEncrypted.nonce, "hex")]); + encryptedKey = await encrypt(key.toString('binary'), dialog); + plainMessage = await encodeWithPassword(privatePlain, message.trim()); + content = cahchaEncrypted.ciphertext; + }else{ + /** + * Это группа, там шифрование устроено иначе + * для групп используется один общий ключ, который + * есть только у участников группы, сам ключ при этом никак + * не отправляется по сети (ведь ID у группы общий и у каждого + * и так есть этот ключ) + */ + const groupKey = await getGroupKey(dialog); + if(!groupKey){ + warn("Group key not found for dialog " + dialog); + return; + } + content = await encodeWithPassword(groupKey, message.trim()); + plainMessage = await encodeWithPassword(privatePlain, message.trim()); + encryptedKey = ""; // В группах не нужен зашифрованный ключ + key = Buffer.from(groupKey); + } + + /** + * Нужно зашифровать ключ еще и нашим ключом, + * чтобы в последствии мы могли расшифровать этот ключ у своих + * же сообщений (смотреть problem_sync.md) + */ + const aesChachaKey = await encodeWithPassword(privatePlain, key.toString('binary')); + + emitDialogMessage({ + dialogId: dialog, + message: { + from_public_key: publicKey, + to_public_key: dialog, + content: content, + timestamp: Date.now(), + readed: publicKey == dialog ? 1 : 0, + chacha_key: "", + from_me: 1, + plain_message: message, + delivered: serverSent ? (publicKey == dialog ? DeliveredMessageState.DELIVERED : DeliveredMessageState.WAITING) : DeliveredMessageState.DELIVERED, + message_id: messageId, + attachments: attachemnts + } as Message + }) + + + let attachmentsMeta : AttachmentMeta[] = []; + for(let i = 0; i < attachemnts.length; i++) { + const attachment = attachemnts[i]; + attachmentsMeta.push({ + id: attachment.id, + type: attachment.type, + preview: attachment.preview + }); + if(attachment.type == AttachmentType.FILE){ + /** + * Обычно вложения дублируются на диск. Так происходит со всем. + * Кроме файлов. Если дублировать файл весом в 2гб на диск отправка будет + * занимать очень много времени. + * К тому же, это приведет к созданию ненужной копии у отправителя + */ + continue; + } + writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, Buffer.from(await encodeWithPassword(privatePlain, attachment.blob)).toString('binary')); + } + + await runQuery(` + INSERT INTO messages + (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [publicKey, dialog, content, Date.now(), publicKey == dialog ? 1 : 0, encryptedKey, 1, plainMessage, publicKey, messageId, publicKey == dialog ? DeliveredMessageState.DELIVERED : ( + (serverSent ? (protocolState != ProtocolState.CONNECTED ? DeliveredMessageState.ERROR : DeliveredMessageState.WAITING) : DeliveredMessageState.DELIVERED) + ), JSON.stringify(attachmentsMeta)]); + updateDialog(dialog); + if(publicKey == "" + || dialog == "" + || publicKey == dialog) { + return; + } + let preparedToNetworkSendAttachements : Attachment[] = await prepareAttachmentsToSend(messageId, dialog, key.toString('utf-8'), attachemnts); + if(attachemnts.length <= 0 && message.trim() == ""){ + runQuery("UPDATE messages SET delivered = ? WHERE message_id = ?", [DeliveredMessageState.ERROR, messageId]); + updateDialog(dialog); + return; + } + + if(!serverSent){ + return; + } + + const packet = new PacketMessage(); + packet.setFromPublicKey(publicKey); + packet.setToPublicKey(dialog); + packet.setContent(content); + packet.setChachaKey(encryptedKey); + packet.setPrivateKey(privateKey); + packet.setMessageId(messageId); + packet.setTimestamp(Date.now()); + packet.setAttachments(preparedToNetworkSendAttachements); + packet.setAesChachaKey(aesChachaKey); + send(packet); + } + + return {sendMessage}; +} \ No newline at end of file diff --git a/app/providers/DialogProvider/useDialog.ts b/app/providers/DialogProvider/useDialog.ts index 4b54615..61c5b63 100644 --- a/app/providers/DialogProvider/useDialog.ts +++ b/app/providers/DialogProvider/useDialog.ts @@ -14,6 +14,7 @@ import { useProtocolState } from "../ProtocolProvider/useProtocolState"; import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; import { useGroups } from "./useGroups"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; +import { usePrepareAttachment } from "../AttachmentProvider/usePrepareAttachment"; export function useDialog() : { messages: Message[]; @@ -34,8 +35,7 @@ export function useDialog() : { throw new Error("useDialog must be used within a DialogProvider"); } const {loading, - messages, - prepareAttachmentsToSend, + messages, clearDialogCache, setMessages, dialog, loadMessagesToTop, loadMessagesToMessageId} = context; @@ -47,6 +47,7 @@ export function useDialog() : { const [protocolState] = useProtocolState(); const {hasGroup, getGroupKey} = useGroups(); const {warn} = useConsoleLogger('useDialog'); + const {prepareAttachmentsToSend} = usePrepareAttachment(); /** * Отправка сообщения в диалог @@ -146,7 +147,7 @@ export function useDialog() : { //98acbbc68f4b2449daf0a39d1b3eab9a3056da5d45b811bbc903e214c21d39643394980231e1a89c811830d870f3354184319665327ca8bd console.info("Sending key for message ", key.toString('hex')); - let preparedToNetworkSendAttachements : Attachment[] = await prepareAttachmentsToSend(key.toString('utf-8'), attachemnts); + let preparedToNetworkSendAttachements : Attachment[] = await prepareAttachmentsToSend(messageId, dialog, key.toString('utf-8'), attachemnts); if(attachemnts.length <= 0 && message.trim() == ""){ runQuery("UPDATE messages SET delivered = ? WHERE message_id = ?", [DeliveredMessageState.ERROR, messageId]); updateDialog(dialog); diff --git a/app/providers/DialogProvider/useDialogsCache.ts b/app/providers/DialogProvider/useDialogsCache.ts index 030c601..0889810 100644 --- a/app/providers/DialogProvider/useDialogsCache.ts +++ b/app/providers/DialogProvider/useDialogsCache.ts @@ -30,7 +30,7 @@ export function useDialogsCache() { const updateAttachmentInDialogCache = (attachment_id: string, blob: string) => { /** - * TODO: Optimize this function to avoid full map if possible + * TODO: Оптимизировать чтобы проходил снизу вверх */ let newCache = dialogsCache.map((cache) => { let newMessages = cache.messages.map((message) => { diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.message.ts b/app/providers/ProtocolProvider/protocol/packets/packet.message.ts index ae61f1f..66ba616 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.message.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.message.ts @@ -5,7 +5,8 @@ export enum AttachmentType { IMAGE = 0, MESSAGES = 1, FILE = 2, - AVATAR = 3 + AVATAR = 3, + CALL } export interface Attachment { diff --git a/app/utils/constructLastMessageTextByAttachments.ts b/app/utils/constructLastMessageTextByAttachments.ts index 6d0faac..d5b0fc2 100644 --- a/app/utils/constructLastMessageTextByAttachments.ts +++ b/app/utils/constructLastMessageTextByAttachments.ts @@ -15,6 +15,8 @@ export const constructLastMessageTextByAttachments = (attachment: string) => { return "$a=File"; case AttachmentType.AVATAR: return "$a=Avatar"; + case AttachmentType.CALL: + return "$a=Call"; default: return "[Unsupported attachment]"; }