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]";
}