From 1572f06ef43687cdb23e534104d188c62c7d127a Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Feb 2026 20:49:55 +0200 Subject: [PATCH 01/67] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=82=D1=87=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BD=D0=B0=D0=B6?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=D1=8F=20ESC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/DialogInput/DialogInput.tsx | 1 + app/components/MessageAttachments/MessageAttachments.tsx | 1 - app/components/ReplyHeader/ReplyHeader.tsx | 6 +----- app/providers/DialogProvider/useReplyMessages.ts | 2 ++ 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/components/DialogInput/DialogInput.tsx b/app/components/DialogInput/DialogInput.tsx index e529a4e..4cb6354 100644 --- a/app/components/DialogInput/DialogInput.tsx +++ b/app/components/DialogInput/DialogInput.tsx @@ -60,6 +60,7 @@ export function DialogInput() { useHotkeys([ ['Esc', () => { setAttachments([]); + deselectAllMessages(); }] ], [], true); diff --git a/app/components/MessageAttachments/MessageAttachments.tsx b/app/components/MessageAttachments/MessageAttachments.tsx index 6446d1e..44c7714 100644 --- a/app/components/MessageAttachments/MessageAttachments.tsx +++ b/app/components/MessageAttachments/MessageAttachments.tsx @@ -42,7 +42,6 @@ export function MessageAttachments(props: MessageAttachmentsProps) { text: props.text, parent: props.parent, } - console.info("Rendering attachment", attachProps); switch (att.type) { case AttachmentType.MESSAGES: return diff --git a/app/components/ReplyHeader/ReplyHeader.tsx b/app/components/ReplyHeader/ReplyHeader.tsx index 23b6c4c..760895c 100644 --- a/app/components/ReplyHeader/ReplyHeader.tsx +++ b/app/components/ReplyHeader/ReplyHeader.tsx @@ -1,7 +1,7 @@ import { useRosettaColors } from "@/app/hooks/useRosettaColors"; import { useReplyMessages } from "@/app/providers/DialogProvider/useReplyMessages"; import { Button, Flex, Modal, Text } from "@mantine/core"; -import { useDisclosure, useHotkeys } from "@mantine/hooks"; +import { useDisclosure } from "@mantine/hooks"; import { IconCornerUpLeft, IconCornerUpRightDouble, IconTrash, IconX } from "@tabler/icons-react"; import classes from "./ReplyHeader.module.css"; import { DialogsList } from "../DialogsList/DialogsList"; @@ -19,10 +19,6 @@ export function ReplyHeader() { const [opened, { open, close }] = useDisclosure(false); const navigate = useNavigate(); const {deleteSelectedMessages} = useDialog(); - - useHotkeys([ - ['Esc', deselectAllMessages] - ], [], true); const onClickForward = () => { open(); diff --git a/app/providers/DialogProvider/useReplyMessages.ts b/app/providers/DialogProvider/useReplyMessages.ts index 4475286..487e8bb 100644 --- a/app/providers/DialogProvider/useReplyMessages.ts +++ b/app/providers/DialogProvider/useReplyMessages.ts @@ -35,6 +35,7 @@ export function useReplyMessages() { const {dialog} = context; const selectMessage = (message : MessageReply) => { + console.info("-> ", replyMessages); console.info(message); if(replyMessages.publicKey != dialog){ /** @@ -70,6 +71,7 @@ export function useReplyMessages() { } const deselectAllMessages = () => { + console.info("Deselecting all messages"); setReplyMessages({ publicKey: "", messages: [] From fe418dabc97ab7cd973fca8ae7c80cb40d9e61eb Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Feb 2026 22:25:06 +0200 Subject: [PATCH 02/67] =?UTF-8?q?=D0=9F=D1=80=D0=B8=20=D0=BA=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=BD=D0=B0=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BE=D0=BD=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F,=20=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=83=D1=85=D0=BE=D0=B4=D0=B8=D1=82=20=D0=B2=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=20=D0=BA=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/Messages/Message.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Messages/Message.tsx b/app/components/Messages/Message.tsx index 8868292..9e21c66 100644 --- a/app/components/Messages/Message.tsx +++ b/app/components/Messages/Message.tsx @@ -213,7 +213,7 @@ export function Message(props: MessageProps) { userSelect: 'text', fontSize: '13px', color: messageStyle == MessageStyle.BUBBLES ? (computedTheme == 'light' ? (props.parent?.from_me ? 'white' : 'black') : 'white') : (computedTheme == 'light' ? 'black' : 'white') - }} ml={props.avatar_no_render ? 50 : undefined}> + }} ml={props.avatar_no_render ? 50 : undefined} onDoubleClick={(e) => e.stopPropagation()}> @@ -302,7 +302,7 @@ export function Message(props: MessageProps) { userSelect: 'text', fontSize: '14px', color: props.from_me ? 'white' : (computedTheme == 'light' ? 'black' : 'white') - }}> + }} onDoubleClick={(e) => e.stopPropagation()}> )} From a9ce892ea22f73dba6a5c2e27b54721eaa5180db Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Feb 2026 22:26:40 +0200 Subject: [PATCH 03/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/version.ts b/app/version.ts index 36c3ecf..9ff6ed3 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,13 +1,8 @@ -export const APP_VERSION = "1.0.5"; +export const APP_VERSION = "1.0.6"; export const CORE_MIN_REQUIRED_VERSION = "1.5.0"; export const RELEASE_NOTICE = ` -**Обновление v1.0.5** :emoji_1f631: -- Оптимизирован код ядра -- Исправление ошибки с системой обновления в результате гонки потоков в ядре -- Исправление уведомлений при синхронизации -- Анимация перемещения диалогов -- Оптимизирован код вложений -- Исправлен скролл при подгрузке сообщений сверху -- Ускорена загрузка диалогов при большом количестве тяжелых изображений +**Обновление v1.0.6** :emoji_1f631: +- Исправлена очистка сообщения при нажатии ESC +- При клике на текст в сообщении теперь сообщение не уходит в ответ `; \ No newline at end of file From 8952fe43e81dc4415d66d69a0bf432f8f65320f5 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Mon, 23 Feb 2026 13:20:40 +0200 Subject: [PATCH 04/67] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D1=83=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B2=20=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B5=20=D0=BD=D0=B5=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/DialogProvider/useDialogFiber.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index b3710b3..8373d2d 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -328,7 +328,7 @@ export function useDialogFiber() { addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); } }); - }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); + }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]); /** * Обработчик личных сообщений */ @@ -445,9 +445,9 @@ export function useDialogFiber() { * чтобы когда приходит пачка сообщений с сервера в момент того как * пользователь был неактивен, не слать уведомления по всем этим сообщениям */ - if (!muted.includes(fromPublicKey) || protocolState != ProtocolState.SYNCHRONIZATION) { + if (!muted.includes(fromPublicKey) && protocolState != ProtocolState.SYNCHRONIZATION) { /** - * Если пользователь в муте или сейчас идет синхронизация - не отправляем уведомление + * Если пользователь в муте И сейчас не идет синхронизация, то не отправляем уведомление */ notify("New message", "You have a new message"); } @@ -457,7 +457,7 @@ export function useDialogFiber() { addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); } }); - }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); + }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]); /** * Обработчик синхронизации прочтения личных сообщений From bf057c14f48c1f55af00ef0865928a465a7a4c63 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 14:33:47 +0200 Subject: [PATCH 05/67] =?UTF-8?q?=D0=97=D0=B0=D1=89=D0=B8=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=B0=D1=8F=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=B9=20=D0=B8=20=D0=BC=D0=B5=D1=82=D0=B0-=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/DialogProvider/useGroups.ts | 26 +++------ .../DialogProvider/useSynchronize.ts | 53 ++++++++++++++++++- .../protocol/packets/packet.group.join.ts | 18 +++++++ app/servers.ts | 4 +- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/app/providers/DialogProvider/useGroups.ts b/app/providers/DialogProvider/useGroups.ts index 78499d6..1935cdc 100644 --- a/app/providers/DialogProvider/useGroups.ts +++ b/app/providers/DialogProvider/useGroups.ts @@ -162,26 +162,10 @@ export function useGroups() : { const groupId = packet.getGroupId(); info(`Creating group with id ${groupId}`); const encryptKey = generateRandomKey(64); - const secureKey = await encodeWithPassword(privatePlain, encryptKey); - let content = await encodeWithPassword(encryptKey, `$a=Group created`); - let plainMessage = await encodeWithPassword(privatePlain, `$a=Group created`); - await runQuery(` - INSERT INTO groups (account, group_id, title, description, key) VALUES (?, ?, ?, ?, ?) - `, [publicKey, groupId, title, description, secureKey]); - 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, "#group:" + groupId, content, Date.now(), 1, "", 1, plainMessage, publicKey, generateRandomKey(16), - DeliveredMessageState.DELIVERED - , '[]']); - updateDialog("#group:" + groupId); - updateGroupInformation({ - groupId: groupId, - title: title, - description: description - }); - setLoading(false); - navigate(`/main/chat/${prepareForRoute(groupId)}`); + /** + * После создания группы в нее необходимо зайти, в соотвествии с новым протоколом + */ + joinGroup(await constructGroupString(groupId, title, encryptKey, description)); }); } @@ -201,9 +185,11 @@ export function useGroups() : { const groupId = parsed.groupId; const title = parsed.title; const description = parsed.description; + const encodedGroupString = await encodeWithPassword(privatePlain, groupString); const packet = new PacketGroupJoin(); packet.setGroupId(parsed.groupId); + packet.setGroupString(encodedGroupString); send(packet); setLoading(true); diff --git a/app/providers/DialogProvider/useSynchronize.ts b/app/providers/DialogProvider/useSynchronize.ts index 0456a64..332ab79 100644 --- a/app/providers/DialogProvider/useSynchronize.ts +++ b/app/providers/DialogProvider/useSynchronize.ts @@ -8,6 +8,16 @@ import { useSender } from "../ProtocolProvider/useSender"; import { usePacket } from "../ProtocolProvider/usePacket"; import { whenFinish } from "./dialogQueue"; import { useProtocol } from "../ProtocolProvider/useProtocol"; +import { PacketGroupJoin } from "../ProtocolProvider/protocol/packets/packet.group.join"; +import { useGroups } from "./useGroups"; +import { decodeWithPassword, encodeWithPassword } from "@/app/workers/crypto/crypto"; +import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; +import { GroupStatus } from "../ProtocolProvider/protocol/packets/packet.group.invite.info"; +import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; +import { useNavigate } from "react-router-dom"; +import { useDialogsList } from "../DialogListProvider/useDialogsList"; +import { useUpdateGroupInformation } from "../InformationProvider/useUpdateGroupInformation"; +import { useGroupInviteStatus } from "./useGroupInviteStatus"; /** * Хук отвечает за синхронизацию сообщений, запрос синхронизации @@ -19,7 +29,14 @@ export function useSynchronize() { const publicKey = usePublicKey(); const send = useSender(); const {protocol} = useProtocol(); + const {parseGroupString} = useGroups(); + const privatePlain = usePrivatePlain(); + const {error, info} = useConsoleLogger('useSynchronize'); + const {setInviteStatusByGroupId} = useGroupInviteStatus(''); + const updateGroupInformation = useUpdateGroupInformation(); + const {updateDialog} = useDialogsList(); + useEffect(() => { if(protocol.handshakeExchangeComplete){ trySync(); @@ -38,15 +55,47 @@ export function useSynchronize() { send(packet); } + /** + * Пакет приходит либо при входе в группу (но там используется слушатель once), либо при + * синхронизации. В данном случае этот пакет прийдет только при синхронизации + */ + usePacket(20, async (packet: PacketGroupJoin) => { + const decryptedGroupString = await decodeWithPassword(privatePlain, packet.getGroupString()); + const parsed = await parseGroupString(decryptedGroupString); + if(!parsed){ + error("Received invalid group string, skipping"); + return; + } + const groupStatus = packet.getGroupStatus(); + if(groupStatus != GroupStatus.JOINED){ + error("Cannot sync group that is not joined, skipping"); + return; + } + const secureKey = await encodeWithPassword(privatePlain, parsed.encryptKey); + await runQuery(` + INSERT INTO groups (account, group_id, title, description, key) VALUES (?, ?, ?, ?, ?) + `, [publicKey, parsed.groupId, parsed.title, parsed.description, secureKey]); + updateDialog("#group:" + parsed.groupId); + setInviteStatusByGroupId(parsed.groupId, GroupStatus.JOINED); + updateGroupInformation({ + groupId: parsed.groupId, + title: parsed.title, + description: parsed.description + }); + info("Group synchronized " + parsed.groupId); + }, [publicKey]); + usePacket(25, async (packet: PacketSync) => { const status = packet.getStatus(); if(status == SyncStatus.BATCH_START){ setProtocolState(ProtocolState.SYNCHRONIZATION); } if(status == SyncStatus.BATCH_END){ - console.info("Batch start"); + /** + * Этот Promise ждет пока все сообщения синхронизируются и обработаются, только + * после этого + */ await whenFinish(); - console.info("Batch finished"); await runQuery( "INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " + "ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?", diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts b/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts index 89c271c..2f9f71c 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts @@ -12,6 +12,14 @@ export class PacketGroupJoin extends Packet { private groupId: string = ""; private groupStatus: GroupStatus = GroupStatus.NOT_JOINED; + /** + * Строка группы, которая содержит информацию о группе, такую как ее название, описание и ключ + * Строка зашифрована обратимым шифрованием, где ключом выступает - реальный приватный ключ + * входящего в группу клиента. Нужно это для будущей синхронзации, так как клиенту на его другом + * устройстве нужно получить ключ группы и ее информацию. Сервер расшифровать эту строку не может. Эту + * строку может расшифровать только клиент, так как она зашифрована его приватным ключом + */ + private groupString: string = ""; public getPacketId(): number { return 0x14; @@ -20,6 +28,7 @@ export class PacketGroupJoin extends Packet { public _receive(stream: Stream): void { this.groupId = stream.readString(); this.groupStatus = stream.readInt8(); + this.groupString = stream.readString(); } public _send(): Promise | Stream { @@ -27,6 +36,7 @@ export class PacketGroupJoin extends Packet { stream.writeInt16(this.getPacketId()); stream.writeString(this.groupId); stream.writeInt8(this.groupStatus); + stream.writeString(this.groupString); return stream; } @@ -45,5 +55,13 @@ export class PacketGroupJoin extends Packet { public getGroupStatus(): GroupStatus { return this.groupStatus; } + + public setGroupString(groupString: string) { + this.groupString = groupString; + } + + public getGroupString(): string { + return this.groupString; + } } \ No newline at end of file diff --git a/app/servers.ts b/app/servers.ts index aa52cbb..53d4fca 100644 --- a/app/servers.ts +++ b/app/servers.ts @@ -1,8 +1,8 @@ export const SERVERS = [ //'wss://cdn.rosetta-im.com', //'ws://10.211.55.2:3000', - //'ws://127.0.0.1:3000', - 'wss://wss.rosetta.im' + 'ws://127.0.0.1:3000', + //'wss://wss.rosetta.im' ]; export function selectServer(): string { From fbc4f73f3d57e129f8c7784473290fba2b32fab2 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 15:42:15 +0200 Subject: [PATCH 06/67] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=BE=D1=80=D0=B3=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DialogProvider/useDialogFiber.ts | 160 +----------------- .../DialogProvider/useSynchronize.ts | 154 ++++++++++++++++- .../DialogProvider/useUpdateSyncTime.ts | 33 ++++ 3 files changed, 186 insertions(+), 161 deletions(-) create mode 100644 app/providers/DialogProvider/useUpdateSyncTime.ts diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index 8373d2d..112bb5c 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -27,6 +27,7 @@ import { useMentions } from "../DialogStateProvider.tsx/useMentions"; import { runTaskInQueue } from "./dialogQueue"; import { useProtocolState } from "../ProtocolProvider/useProtocolState"; import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; +import { useUpdateSyncTime } from "./useUpdateSyncTime"; /** * При вызове будет запущен "фоновый" обработчик @@ -53,27 +54,7 @@ export function useDialogFiber() { const [userInfo] = useUserInformation(publicKey); const { pushMention } = useMentions(); const [protocolState] = useProtocolState(); - - /** - * Обновляет время последней синхронизации для аккаунта - * @param timestamp время - */ - const updateSyncTime = async (timestamp: number) => { - if(protocolState == ProtocolState.SYNCHRONIZATION){ - /** - * Если сейчас идет синхронизация то чтобы при синхронизации - * не создавать нагрузку на базу данных - * по постоянному обновлению, обновляем базу один раз - когда - * приходит пакет о том что синхронизация закончилась - */ - return; - } - await runQuery( - "INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " + - "ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?", - [publicKey, timestamp, timestamp, publicKey] - ); - }; + const updateSyncTime = useUpdateSyncTime(); /** * Лог @@ -82,101 +63,6 @@ export function useDialogFiber() { info("Starting passive fiber for dialog packets"); }, []); - /** - * Нам приходят сообщения от себя самих же при синхронизации - * нужно обрабатывать их особым образом соотвественно - * - * Метод нужен для синхронизации своих сообщений - */ - usePacket(0x06, async (packet: PacketMessage) => { - runTaskInQueue(async () => { - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); - const aesChachaKey = packet.getAesChachaKey(); - const content = packet.getContent(); - const timestamp = packet.getTimestamp(); - const messageId = packet.getMessageId(); - if (fromPublicKey != publicKey) { - /** - * Игнорируем если это не сообщение от нас - */ - return; - } - - const chachaKey = await decodeWithPassword(privatePlain, aesChachaKey); - const chachaDecryptedKey = Buffer.from(chachaKey, "binary"); - const key = chachaDecryptedKey.slice(0, 32); - const nonce = chachaDecryptedKey.slice(32); - const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); - await updateSyncTime(timestamp); - let attachmentsMeta: any[] = []; - let messageAttachments: Attachment[] = []; - for (let i = 0; i < packet.getAttachments().length; i++) { - const attachment = packet.getAttachments()[i]; - log("Attachment received id " + attachment.id + " type " + attachment.type); - - let nextLength = messageAttachments.push({ - ...attachment, - blob: "" - }); - - if (attachment.type == AttachmentType.MESSAGES) { - /** - * Этот тип вложения приходит сразу в blob и не нуждается - * в последующем скачивании - */ - const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob); - writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, - Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); - messageAttachments[nextLength - 1].blob = decryptedBlob; - } - - attachmentsMeta.push({ - id: attachment.id, - type: attachment.type, - preview: attachment.preview - }); - } - - const newMessage: Message = { - from_public_key: fromPublicKey, - to_public_key: toPublicKey, - content: content, - timestamp: timestamp, - readed: 1, //сообщение прочитано - chacha_key: chachaDecryptedKey.toString('utf-8'), - from_me: 1, //сообщение от нас - plain_message: (decryptedContent as string), - delivered: DeliveredMessageState.DELIVERED, - message_id: messageId, - attachments: messageAttachments - }; - - 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [fromPublicKey, - toPublicKey, - content, - timestamp, - 0, //по умолчанию не прочитаны - "sync:" + aesChachaKey, - 1, //Свои же сообщения всегда от нас - await encodeWithPassword(privatePlain, decryptedContent), - publicKey, - messageId, - DeliveredMessageState.DELIVERED, - JSON.stringify(attachmentsMeta)]); - - updateDialog(toPublicKey); - - let dialogCache = getDialogCache(toPublicKey); - if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { - addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); - } - }); - }, [privatePlain, currentDialogPublicKeyView]); - /** * Обработчик сообщений для группы */ @@ -459,47 +345,6 @@ export function useDialogFiber() { }); }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]); - /** - * Обработчик синхронизации прочтения личных сообщений - */ - usePacket(0x07, async (packet: PacketRead) => { - runTaskInQueue(async () => { - if (hasGroup(packet.getToPublicKey())) { - /** - * Если это относится к группам, то игнорируем здесь, - * для этого есть отдельный слушатель usePacket ниже - */ - return; - } - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); - if (fromPublicKey != publicKey) { - /** - * Игнорируем если это не синхронизация нашего прочтения - */ - return; - } - console.info("PACKED_READ_SYNC"); - await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, - [toPublicKey, fromPublicKey, publicKey]); - - console.info("updating with params ", [fromPublicKey, toPublicKey, publicKey]); - updateDialog(toPublicKey); - log("Read sync packet from other device"); - addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { - if (message.from_public_key == toPublicKey && !message.readed) { - console.info("Marking message as read in cache for dialog with " + fromPublicKey); - console.info({ fromPublicKey, toPublicKey }); - return { - ...message, - readed: 1 - } - } - return message; - })); - }); - }, [updateDialog, publicKey]); - /** * Обработчик прочтения личных сообщений */ @@ -540,6 +385,7 @@ export function useDialogFiber() { })); }); }, [updateDialog, publicKey]); + /** * Обработчик прочтения групповых сообщений */ diff --git a/app/providers/DialogProvider/useSynchronize.ts b/app/providers/DialogProvider/useSynchronize.ts index 332ab79..b7248d3 100644 --- a/app/providers/DialogProvider/useSynchronize.ts +++ b/app/providers/DialogProvider/useSynchronize.ts @@ -6,18 +6,25 @@ import { usePublicKey } from "../AccountProvider/usePublicKey"; import { PacketSync, SyncStatus } from "../ProtocolProvider/protocol/packets/packet.sync"; import { useSender } from "../ProtocolProvider/useSender"; import { usePacket } from "../ProtocolProvider/usePacket"; -import { whenFinish } from "./dialogQueue"; +import { runTaskInQueue, whenFinish } from "./dialogQueue"; import { useProtocol } from "../ProtocolProvider/useProtocol"; import { PacketGroupJoin } from "../ProtocolProvider/protocol/packets/packet.group.join"; import { useGroups } from "./useGroups"; -import { decodeWithPassword, encodeWithPassword } from "@/app/workers/crypto/crypto"; +import { chacha20Decrypt, decodeWithPassword, encodeWithPassword, generateMd5 } from "@/app/workers/crypto/crypto"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; import { GroupStatus } from "../ProtocolProvider/protocol/packets/packet.group.invite.info"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; -import { useNavigate } from "react-router-dom"; import { useDialogsList } from "../DialogListProvider/useDialogsList"; import { useUpdateGroupInformation } from "../InformationProvider/useUpdateGroupInformation"; import { useGroupInviteStatus } from "./useGroupInviteStatus"; +import { Attachment, AttachmentType, PacketMessage } from "../ProtocolProvider/protocol/packets/packet.message"; +import { useUpdateSyncTime } from "./useUpdateSyncTime"; +import { useFileStorage } from "@/app/hooks/useFileStorage"; +import { DeliveredMessageState, Message } from "./DialogProvider"; +import { MESSAGE_MAX_LOADED } from "@/app/constants"; +import { useMemory } from "../MemoryProvider/useMemory"; +import { useDialogsCache } from "./useDialogsCache"; +import { PacketRead } from "../ProtocolProvider/protocol/packets/packet.read"; /** * Хук отвечает за синхронизацию сообщений, запрос синхронизации @@ -29,12 +36,16 @@ export function useSynchronize() { const publicKey = usePublicKey(); const send = useSender(); const {protocol} = useProtocol(); - const {parseGroupString} = useGroups(); + const {parseGroupString, hasGroup} = useGroups(); const privatePlain = usePrivatePlain(); const {error, info} = useConsoleLogger('useSynchronize'); const {setInviteStatusByGroupId} = useGroupInviteStatus(''); const updateGroupInformation = useUpdateGroupInformation(); const {updateDialog} = useDialogsList(); + const updateSyncTime = useUpdateSyncTime(); + const {writeFile} = useFileStorage(); + const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache(); + const [currentDialogPublicKeyView, __] = useMemory("current-dialog-public-key-view", "", true); useEffect(() => { @@ -111,4 +122,139 @@ export function useSynchronize() { setProtocolState(ProtocolState.CONNECTED); } }, [publicKey]); + + /** + * Нам приходят сообщения от себя самих же при синхронизации + * нужно обрабатывать их особым образом соотвественно + * + * Метод нужен для синхронизации своих сообщений + */ + usePacket(0x06, async (packet: PacketMessage) => { + runTaskInQueue(async () => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + const aesChachaKey = packet.getAesChachaKey(); + const content = packet.getContent(); + const timestamp = packet.getTimestamp(); + const messageId = packet.getMessageId(); + if (fromPublicKey != publicKey) { + /** + * Игнорируем если это не сообщение от нас + */ + return; + } + + const chachaKey = await decodeWithPassword(privatePlain, aesChachaKey); + const chachaDecryptedKey = Buffer.from(chachaKey, "binary"); + const key = chachaDecryptedKey.slice(0, 32); + const nonce = chachaDecryptedKey.slice(32); + const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); + await updateSyncTime(timestamp); + let attachmentsMeta: any[] = []; + let messageAttachments: Attachment[] = []; + for (let i = 0; i < packet.getAttachments().length; i++) { + const attachment = packet.getAttachments()[i]; + + + let nextLength = messageAttachments.push({ + ...attachment, + blob: "" + }); + + if (attachment.type == AttachmentType.MESSAGES) { + /** + * Этот тип вложения приходит сразу в blob и не нуждается + * в последующем скачивании + */ + const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob); + writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, + Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); + messageAttachments[nextLength - 1].blob = decryptedBlob; + } + + attachmentsMeta.push({ + id: attachment.id, + type: attachment.type, + preview: attachment.preview + }); + } + + const newMessage: Message = { + from_public_key: fromPublicKey, + to_public_key: toPublicKey, + content: content, + timestamp: timestamp, + readed: 1, //сообщение прочитано + chacha_key: chachaDecryptedKey.toString('utf-8'), + from_me: 1, //сообщение от нас + plain_message: (decryptedContent as string), + delivered: DeliveredMessageState.DELIVERED, + message_id: messageId, + attachments: messageAttachments + }; + + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [fromPublicKey, + toPublicKey, + content, + timestamp, + 0, //по умолчанию не прочитаны + "sync:" + aesChachaKey, + 1, //Свои же сообщения всегда от нас + await encodeWithPassword(privatePlain, decryptedContent), + publicKey, + messageId, + DeliveredMessageState.DELIVERED, + JSON.stringify(attachmentsMeta)]); + + updateDialog(toPublicKey); + + let dialogCache = getDialogCache(toPublicKey); + if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { + addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); + } + }); + }, [privatePlain, currentDialogPublicKeyView]); + + /** + * Обработчик синхронизации прочтения личных сообщений + */ + usePacket(0x07, async (packet: PacketRead) => { + runTaskInQueue(async () => { + if (hasGroup(packet.getToPublicKey())) { + /** + * Если это относится к группам, то игнорируем здесь, + * для этого есть отдельный слушатель usePacket ниже + */ + return; + } + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (fromPublicKey != publicKey) { + /** + * Игнорируем если это не синхронизация нашего прочтения + */ + return; + } + console.info("PACKED_READ_SYNC"); + await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, + [toPublicKey, fromPublicKey, publicKey]); + + console.info("updating with params ", [fromPublicKey, toPublicKey, publicKey]); + updateDialog(toPublicKey); + addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { + if (message.from_public_key == toPublicKey && !message.readed) { + console.info("Marking message as read in cache for dialog with " + fromPublicKey); + console.info({ fromPublicKey, toPublicKey }); + return { + ...message, + readed: 1 + } + } + return message; + })); + }); + }, [updateDialog, publicKey]); } \ No newline at end of file diff --git a/app/providers/DialogProvider/useUpdateSyncTime.ts b/app/providers/DialogProvider/useUpdateSyncTime.ts new file mode 100644 index 0000000..45ad845 --- /dev/null +++ b/app/providers/DialogProvider/useUpdateSyncTime.ts @@ -0,0 +1,33 @@ +import { usePublicKey } from "../AccountProvider/usePublicKey"; +import { useDatabase } from "../DatabaseProvider/useDatabase"; +import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; +import { useProtocolState } from "../ProtocolProvider/useProtocolState"; + +export function useUpdateSyncTime() : (timestamp: number) => Promise { + const [protocolState] = useProtocolState(); + const {runQuery} = useDatabase(); + const publicKey = usePublicKey(); + + /** + * Обновляет время последней синхронизации для аккаунта + * @param timestamp время + */ + const updateSyncTime = async (timestamp: number) => { + if(protocolState == ProtocolState.SYNCHRONIZATION){ + /** + * Если сейчас идет синхронизация то чтобы при синхронизации + * не создавать нагрузку на базу данных + * по постоянному обновлению, обновляем базу один раз - когда + * приходит пакет о том что синхронизация закончилась + */ + return; + } + await runQuery( + "INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " + + "ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?", + [publicKey, timestamp, timestamp, publicKey] + ); + }; + + return updateSyncTime; +} \ No newline at end of file From 089fa055d3aa6e72febf31ef7a3bb7a8e7c8801b Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 16:06:21 +0200 Subject: [PATCH 07/67] =?UTF-8?q?=D0=A1=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=BF=D0=BF=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DialogProvider/useSynchronize.ts | 131 +++++++++++++++++- 1 file changed, 125 insertions(+), 6 deletions(-) diff --git a/app/providers/DialogProvider/useSynchronize.ts b/app/providers/DialogProvider/useSynchronize.ts index b7248d3..1692014 100644 --- a/app/providers/DialogProvider/useSynchronize.ts +++ b/app/providers/DialogProvider/useSynchronize.ts @@ -21,31 +21,39 @@ import { Attachment, AttachmentType, PacketMessage } from "../ProtocolProvider/p import { useUpdateSyncTime } from "./useUpdateSyncTime"; import { useFileStorage } from "@/app/hooks/useFileStorage"; import { DeliveredMessageState, Message } from "./DialogProvider"; -import { MESSAGE_MAX_LOADED } from "@/app/constants"; +import { MESSAGE_MAX_LOADED, TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD } from "@/app/constants"; import { useMemory } from "../MemoryProvider/useMemory"; import { useDialogsCache } from "./useDialogsCache"; import { PacketRead } from "../ProtocolProvider/protocol/packets/packet.read"; +import { useLogger } from "@/app/hooks/useLogger"; +import { useIdle } from "@mantine/hooks"; +import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState"; +import { useWindowFocus } from "@/app/hooks/useWindowFocus"; /** * Хук отвечает за синхронизацию сообщений, запрос синхронизации * при подключении */ export function useSynchronize() { - const [_, setProtocolState] = useProtocolState(); + const [protocolState, setProtocolState] = useProtocolState(); const {getQuery, runQuery} = useDatabase(); const publicKey = usePublicKey(); const send = useSender(); const {protocol} = useProtocol(); - const {parseGroupString, hasGroup} = useGroups(); + const {parseGroupString, hasGroup, getGroupKey} = useGroups(); const privatePlain = usePrivatePlain(); const {error, info} = useConsoleLogger('useSynchronize'); + const log = useLogger('useSynchronize'); const {setInviteStatusByGroupId} = useGroupInviteStatus(''); const updateGroupInformation = useUpdateGroupInformation(); const {updateDialog} = useDialogsList(); + const idle = useIdle(TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD * 1000); const updateSyncTime = useUpdateSyncTime(); const {writeFile} = useFileStorage(); const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache(); const [currentDialogPublicKeyView, __] = useMemory("current-dialog-public-key-view", "", true); + const [viewState] = useViewPanelsState(); + const focused = useWindowFocus(); useEffect(() => { @@ -238,15 +246,12 @@ export function useSynchronize() { */ return; } - console.info("PACKED_READ_SYNC"); await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]); - console.info("updating with params ", [fromPublicKey, toPublicKey, publicKey]); updateDialog(toPublicKey); addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { if (message.from_public_key == toPublicKey && !message.readed) { - console.info("Marking message as read in cache for dialog with " + fromPublicKey); console.info({ fromPublicKey, toPublicKey }); return { ...message, @@ -257,4 +262,118 @@ export function useSynchronize() { })); }); }, [updateDialog, publicKey]); + + /** + * Обработчик сообщений для синхронизации своих же сообщений в группе + */ + usePacket(0x06, async (packet: PacketMessage) => { + runTaskInQueue(async () => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + const content = packet.getContent(); + const timestamp = packet.getTimestamp(); + const messageId = packet.getMessageId(); + if (!hasGroup(toPublicKey)) { + /** + * Если это личное сообщение, то игнорируем его здесь + * для него есть отдельный слушатель usePacket (снизу) + */ + return; + } + if (fromPublicKey != publicKey) { + /** + * Игнорируем если это сообщения не от нас + */ + return; + } + await updateSyncTime(timestamp); + 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 attachmentsMeta: any[] = []; + let messageAttachments: Attachment[] = []; + for (let i = 0; i < packet.getAttachments().length; i++) { + const attachment = packet.getAttachments()[i]; + log("Attachment received id " + attachment.id + " type " + attachment.type); + + let nextLength = messageAttachments.push({ + ...attachment, + blob: "" + }); + + if (attachment.type == AttachmentType.MESSAGES) { + /** + * Этот тип вложения приходит сразу в blob и не нуждается + * в последующем скачивании + */ + const decryptedBlob = await decodeWithPassword(groupKey, attachment.blob); + writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, + Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); + messageAttachments[nextLength - 1].blob = decryptedBlob; + } + + attachmentsMeta.push({ + id: attachment.id, + type: attachment.type, + preview: attachment.preview + }); + } + + 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: messageAttachments + }; + + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [fromPublicKey, + toPublicKey, + content, + timestamp, + /**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */ + (currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, + '', + 0, + await encodeWithPassword(privatePlain, decryptedContent), + publicKey, + messageId, + DeliveredMessageState.DELIVERED, + JSON.stringify(attachmentsMeta)]); + + /** + * Так как у нас в toPublicKey приходит ID группы, + * то обновляем диалог по этому ID, а не по fromPublicKey + * как это сделано в личных сообщениях + */ + updateDialog(toPublicKey); + + let dialogCache = getDialogCache(toPublicKey); + if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { + addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); + } + }); + }, [updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]); } \ No newline at end of file From 785406671c093bd14468e338994de20c981882db Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 17:33:26 +0200 Subject: [PATCH 08/67] =?UTF-8?q?=D0=A1=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=BF=D0=BF=D0=B0=D1=85=20=D0=B8=20=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DialogProvider/DialogProvider.tsx | 81 +++++++++++++++++++ .../DialogProvider/useDialogFiber.ts | 13 ++- .../DialogProvider/useSynchronize.ts | 57 +++++++++++-- 3 files changed, 141 insertions(+), 10 deletions(-) diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 38affed..94b3ce9 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -466,6 +466,87 @@ export function DialogProvider(props: DialogProviderProps) { setMessages((prev) => ([...prev, newMessage])); }, [privatePlain]); + /** + * Обработчик сообщений для синхронизации своих же сообщений в группе + */ + usePacket(0x06, async (packet: PacketMessage) => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + + if(fromPublicKey != publicKey){ + /** + * Это не синхронизация, игнорируем ее в этом обработчике + */ + return; + } + + if(toPublicKey != props.dialog){ + /** + * Исправление кросс диалогового сообщения + */ + return; + } + + if(!hasGroup(props.dialog)){ + /** + * Если это не групповое сообщение, то для него есть + * другой обработчик выше + */ + return; + } + const content = packet.getContent(); + const timestamp = packet.getTimestamp(); + /** + * Генерация рандомного ID сообщения по SEED нужна для того, + * чтобы сообщение записанное здесь в стек сообщений совпадало + * с тем что записывается в БД в файле useDialogFiber.ts + */ + const messageId = packet.getMessageId(); + + 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: 0, + chacha_key: groupKey, + from_me: 1, + plain_message: decryptedContent, + delivered: DeliveredMessageState.DELIVERED, + message_id: messageId, + attachments: attachments + }; + + setMessages((prev) => ([...prev, newMessage])); + }, [messages, idle, props.dialog]); + /** * Обработчик для личных сообщений */ diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index 112bb5c..c2399ce 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -391,15 +391,22 @@ export function useDialogFiber() { */ usePacket(0x07, async (packet: PacketRead) => { runTaskInQueue(async () => { - if (!hasGroup(packet.getToPublicKey())) { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (!hasGroup(toPublicKey)) { /** * Если это не относится к группам, то игнорируем здесь, * для этого есть отдельный слушатель usePacket выше */ return; } - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); + if(fromPublicKey == publicKey){ + /** + * Игнорируем если это наше прочтение + * которое получается при синхронизации + */ + return; + } await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]); await updateSyncTime(Date.now()); updateDialog(toPublicKey); diff --git a/app/providers/DialogProvider/useSynchronize.ts b/app/providers/DialogProvider/useSynchronize.ts index 1692014..82c5bef 100644 --- a/app/providers/DialogProvider/useSynchronize.ts +++ b/app/providers/DialogProvider/useSynchronize.ts @@ -27,7 +27,7 @@ import { useDialogsCache } from "./useDialogsCache"; import { PacketRead } from "../ProtocolProvider/protocol/packets/packet.read"; import { useLogger } from "@/app/hooks/useLogger"; import { useIdle } from "@mantine/hooks"; -import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState"; +import { useViewPanelsState } from "@/app/hooks/useViewPanelsState"; import { useWindowFocus } from "@/app/hooks/useWindowFocus"; /** @@ -145,6 +145,13 @@ export function useSynchronize() { const content = packet.getContent(); const timestamp = packet.getTimestamp(); const messageId = packet.getMessageId(); + if(hasGroup(toPublicKey)){ + /** + * Игнорируем если это сообщение для группы, для них есть отдельный слушатель usePacket ниже + */ + return; + } + if (fromPublicKey != publicKey) { /** * Игнорируем если это не сообщение от нас @@ -262,6 +269,43 @@ export function useSynchronize() { })); }); }, [updateDialog, publicKey]); + + /** + * Обработчик синхронизации прочтения групповых сообщений + */ + usePacket(0x07, async (packet: PacketRead) => { + runTaskInQueue(async () => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (!hasGroup(toPublicKey)) { + /** + * Если это не относится к группам, то игнорируем здесь, + * для этого есть отдельный слушатель usePacket выше + */ + return; + } + if(fromPublicKey != publicKey){ + /** + * Игнорируем если это наше прочтение + * которое получается при синхронизации + */ + return; + } + await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key != ? AND account = ?`, + [toPublicKey, publicKey, publicKey]); + await updateSyncTime(Date.now()); + updateDialog(toPublicKey); + addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => { + if (!message.readed && message.from_public_key != publicKey) { + return { + ...message, + readed: 1 + } + } + return message; + })); + }); + }, [updateDialog]); /** * Обработчик сообщений для синхронизации своих же сообщений в группе @@ -337,9 +381,9 @@ export function useSynchronize() { to_public_key: toPublicKey, content: content, timestamp: timestamp, - readed: idle ? 0 : 1, + readed: 0, chacha_key: groupKey, - from_me: fromPublicKey == publicKey ? 1 : 0, + from_me: 1, plain_message: decryptedContent, delivered: DeliveredMessageState.DELIVERED, message_id: messageId, @@ -353,10 +397,9 @@ export function useSynchronize() { toPublicKey, content, timestamp, - /**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */ - (currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, - '', - 0, + 0, //по умолчанию не прочитаны + "", + 1, //Свои же сообщения всегда от нас await encodeWithPassword(privatePlain, decryptedContent), publicKey, messageId, From 453cc55fc05eb972e05ede33331b2a3262ff9988 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 18:17:48 +0200 Subject: [PATCH 09/67] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D1=81=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=B8=D0=BD=D1=85=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/DialogProvider/DialogProvider.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 94b3ce9..adfdc44 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -420,13 +420,18 @@ export function DialogProvider(props: DialogProviderProps) { const timestamp = packet.getTimestamp(); const messageId = packet.getMessageId(); - if(fromPublicKey != publicKey){ /** * Игнорируем если это не сообщение от нас */ return; } + if(hasGroup(toPublicKey)){ + /** + * Есть другой обработчик для синхронизации групп + */ + return; + } if(toPublicKey != props.dialog) { /** * Игнорируем если это не сообщение для этого диалога From f1fb7ba252e905f09fe5e83ffc3577aa9ec2d8f6 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 18:31:04 +0200 Subject: [PATCH 10/67] =?UTF-8?q?=D0=A1=D0=BA=D1=80=D1=8B=D0=B2=D0=B0?= =?UTF-8?q?=D0=B5=D0=BC=20=D1=81=D1=87=D0=B5=D1=82=D1=87=D0=B8=D0=BA=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=BF=D1=80=D0=BE=D1=87=D0=B8=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/Dialog/Dialog.tsx | 5 ++++- app/servers.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/components/Dialog/Dialog.tsx b/app/components/Dialog/Dialog.tsx index d83ed15..9c4137c 100644 --- a/app/components/Dialog/Dialog.tsx +++ b/app/components/Dialog/Dialog.tsx @@ -18,6 +18,8 @@ import { useDialogInfo } from "@/app/providers/DialogListProvider/useDialogInfo" import { useDialogContextMenu } from "@/app/hooks/useDialogContextMenu"; import { useDialogPin } from "@/app/providers/DialogStateProvider.tsx/useDialogPin"; import { useDialogMute } from "@/app/providers/DialogStateProvider.tsx/useDialogMute"; +import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState"; +import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider"; export interface DialogProps extends DialogRow { onClickDialog: (dialog: string) => void; @@ -51,6 +53,7 @@ export function Dialog(props : DialogProps) { const isInCurrentDialog = props.dialog_id == сurrentDialogPublicKeyView; const currentDialogColor = computedTheme == 'dark' ? '#2a6292' :'#438fd1'; + const [protocolState] = useProtocolState(); usePacket(0x0B, (packet : PacketTyping) => { if(packet.getFromPublicKey() == opponent && packet.getToPublicKey() == publicKey && !fromMe){ @@ -153,7 +156,7 @@ export function Dialog(props : DialogProps) { {!loading && (lastMessage.delivered == DeliveredMessageState.ERROR || (!isMessageDeliveredByTime(lastMessage.timestamp, lastMessage.attachments.length) && lastMessage.delivered != DeliveredMessageState.DELIVERED)) && ( )} - {unreaded > 0 && !lastMessageFromMe && 0 && !lastMessageFromMe && protocolState != ProtocolState.SYNCHRONIZATION && {unreaded > 99 ? '99+' : unreaded}} diff --git a/app/servers.ts b/app/servers.ts index 53d4fca..aa52cbb 100644 --- a/app/servers.ts +++ b/app/servers.ts @@ -1,8 +1,8 @@ export const SERVERS = [ //'wss://cdn.rosetta-im.com', //'ws://10.211.55.2:3000', - 'ws://127.0.0.1:3000', - //'wss://wss.rosetta.im' + //'ws://127.0.0.1:3000', + 'wss://wss.rosetta.im' ]; export function selectServer(): string { From 7c806149b3d6b73e4293e20b86aea39e5ea17045 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 18:44:37 +0200 Subject: [PATCH 11/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/version.ts b/app/version.ts index 9ff6ed3..bc70765 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,8 +1,12 @@ -export const APP_VERSION = "1.0.6"; +export const APP_VERSION = "1.0.7"; export const CORE_MIN_REQUIRED_VERSION = "1.5.0"; export const RELEASE_NOTICE = ` -**Обновление v1.0.6** :emoji_1f631: -- Исправлена очистка сообщения при нажатии ESC -- При клике на текст в сообщении теперь сообщение не уходит в ответ +**Обновление v1.0.7** :emoji_1f631: +- Фикс уведомлений при синхронизации сообщений +- Защищенная синхронизация ключей в группах +- Синхронизация сообщений в группах +- Синхронизация вложений в группах +- Синхронизация индикаторов прочтения +- Улучшенная организация кода и оптимизации `; \ No newline at end of file From 0b3bdface8002f4ebf00de4dfaca705d9c751c2b Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 18:53:04 +0200 Subject: [PATCH 12/67] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BE=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BA=D1=80=D0=BE=D0=BB=D0=BB=D0=B0?= =?UTF-8?q?=20=D1=83=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/ImageViewerProvider/ImageViewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/providers/ImageViewerProvider/ImageViewer.tsx b/app/providers/ImageViewerProvider/ImageViewer.tsx index fe8f30a..b4b5931 100644 --- a/app/providers/ImageViewerProvider/ImageViewer.tsx +++ b/app/providers/ImageViewerProvider/ImageViewer.tsx @@ -87,6 +87,7 @@ export function ImageViewer(props : ImageViewerProps) { // Wheel zoom (zoom to cursor) const onWheel = (e: React.WheelEvent) => { //e.preventDefault(); + e.stopPropagation(); const rect = e.currentTarget.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; From fabd85106d6e7e129bdb592e31b5ab9b03c82718 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 19:11:56 +0200 Subject: [PATCH 13/67] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BB=D0=BE=D0=B3-=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/DialogProvider/DialogProvider.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index adfdc44..377e27d 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -328,7 +328,6 @@ export function DialogProvider(props: DialogProviderProps) { * Обработчик чтения групповых сообщений */ usePacket(0x07, async (packet : PacketRead) => { - info("Read packet received in dialog provider"); const fromPublicKey = packet.getFromPublicKey(); if(fromPublicKey == publicKey){ /** @@ -388,7 +387,6 @@ export function DialogProvider(props: DialogProviderProps) { }, [publicKey]); usePacket(0x08, async (packet : PacketDelivery) => { - info("Delivery packet received in dialog provider"); const fromPublicKey = packet.getToPublicKey(); const messageId = packet.getMessageId(); if(fromPublicKey != props.dialog){ @@ -514,7 +512,6 @@ export function DialogProvider(props: DialogProviderProps) { error("Message dropped because group key not found for group " + toPublicKey); return; } - info("New group message packet received from " + fromPublicKey); let decryptedContent = ''; @@ -673,7 +670,6 @@ export function DialogProvider(props: DialogProviderProps) { error("Message dropped because group key not found for group " + toPublicKey); return; } - info("New group message packet received from " + fromPublicKey); let decryptedContent = ''; From 49d7d9ff628a7904c3c45083c0a6951bcda375f0 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 19:15:58 +0200 Subject: [PATCH 14/67] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=B0=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=BA=D0=B8=20=D1=83=20=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=BF=D0=BF=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/AttachmentProvider/useAttachment.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/providers/AttachmentProvider/useAttachment.ts b/app/providers/AttachmentProvider/useAttachment.ts index d5d88d2..8190277 100644 --- a/app/providers/AttachmentProvider/useAttachment.ts +++ b/app/providers/AttachmentProvider/useAttachment.ts @@ -15,6 +15,7 @@ import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants"; import { useDialog } from "../DialogProvider/useDialog"; import { useCore } from "@/app/hooks/useCore"; import { MessageProps } from "@/app/components/Messages/Message"; +import { useGroups } from "../DialogProvider/useGroups"; export enum DownloadStatus { DOWNLOADED, @@ -39,6 +40,8 @@ export function useAttachment(attachment: Attachment, parentMessage: MessageProp const {info} = useConsoleLogger('useAttachment'); const {updateAttachmentsInMessagesByAttachmentId} = useDialog(); const {getDownloadsPath} = useCore(); + const {hasGroup} = useGroups(); + const {dialog} = useDialog(); const saveAvatar = useSaveAvatar(); @@ -186,7 +189,11 @@ export function useAttachment(attachment: Attachment, parentMessage: MessageProp /** * Устанавливаем аватарку тому, кто ее прислал. */ - saveAvatar(parentMessage.from, avatarPath, decrypted); + let avatarSetTo = parentMessage.from; + if(hasGroup(dialog)){ + avatarSetTo = dialog; + } + saveAvatar(avatarSetTo, avatarPath, decrypted); return; } /** From 4e42eb3c02c4ffebb0f362ed39155a2a659a84b4 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 24 Feb 2026 19:19:00 +0200 Subject: [PATCH 15/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/version.ts b/app/version.ts index bc70765..da72aec 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,12 +1,9 @@ -export const APP_VERSION = "1.0.7"; +export const APP_VERSION = "1.0.8"; export const CORE_MIN_REQUIRED_VERSION = "1.5.0"; export const RELEASE_NOTICE = ` -**Обновление v1.0.7** :emoji_1f631: -- Фикс уведомлений при синхронизации сообщений -- Защищенная синхронизация ключей в группах -- Синхронизация сообщений в группах -- Синхронизация вложений в группах -- Синхронизация индикаторов прочтения -- Улучшенная организация кода и оптимизации +**Обновление v1.0.8** :emoji_1f631: +- Фикс проблемы с загрузкой аватарок в некоторых случаях +- Фикс фонового скролла при увеличении картинки +- Фикс артефактов у картинки `; \ No newline at end of file From 88369171b64ddd3d88264d03b70ed2c5292aa049 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Feb 2026 00:19:49 +0200 Subject: [PATCH 16/67] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D0=BE=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=BC=D0=B5=D0=B6=D0=B4?= =?UTF-8?q?=D1=83=20=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/DialogProvider/useReplyMessages.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/providers/DialogProvider/useReplyMessages.ts b/app/providers/DialogProvider/useReplyMessages.ts index 487e8bb..6083fdf 100644 --- a/app/providers/DialogProvider/useReplyMessages.ts +++ b/app/providers/DialogProvider/useReplyMessages.ts @@ -1,4 +1,4 @@ -import { useContext, useEffect } from "react"; +import { useContext } from "react"; import { useMemory } from "../MemoryProvider/useMemory"; import { Attachment } from "../ProtocolProvider/protocol/packets/packet.message"; import { DialogContext } from "./DialogProvider"; @@ -35,8 +35,6 @@ export function useReplyMessages() { const {dialog} = context; const selectMessage = (message : MessageReply) => { - console.info("-> ", replyMessages); - console.info(message); if(replyMessages.publicKey != dialog){ /** * Сброс выбора сообщений из другого диалога @@ -71,7 +69,6 @@ export function useReplyMessages() { } const deselectAllMessages = () => { - console.info("Deselecting all messages"); setReplyMessages({ publicKey: "", messages: [] @@ -108,16 +105,6 @@ export function useReplyMessages() { })); } - useEffect(() => { - if(replyMessages.publicKey != dialog - && replyMessages.inDialogInput != dialog){ - /** - * Сброс выбора сообщений при смене диалога - */ - deselectAllMessages(); - } - }, [dialog]); - return {replyMessages, translateMessagesToDialogInput, isSelectionInCurrentDialog, From a431b23476489679f2d6c3aa264d4fb7de8bca3c Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Feb 2026 12:19:30 +0200 Subject: [PATCH 17/67] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B7=D1=80=D0=B0?= =?UTF-8?q?=D1=87=D0=BD=D1=8B=D0=BC=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=BB=D0=BE=D0=B6=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ActionAvatar/ActionAvatar.tsx | 1 + app/components/Dialog/Dialog.tsx | 2 +- app/components/Messages/Message.tsx | 4 ++-- app/providers/ImageViewerProvider/ImageViewer.tsx | 1 + app/workers/crypto/crypto.worker.ts | 1 - 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/components/ActionAvatar/ActionAvatar.tsx b/app/components/ActionAvatar/ActionAvatar.tsx index f328e7f..d819f77 100644 --- a/app/components/ActionAvatar/ActionAvatar.tsx +++ b/app/components/ActionAvatar/ActionAvatar.tsx @@ -60,6 +60,7 @@ export function ActionAvatar(props : ActionAvatarProps) { size={120} radius={120} mx="auto" + bg={'#fff'} name={props.title.trim() || props.publicKey} color={'initials'} src={avatars.length > 0 ? diff --git a/app/components/Dialog/Dialog.tsx b/app/components/Dialog/Dialog.tsx index 9c4137c..450d759 100644 --- a/app/components/Dialog/Dialog.tsx +++ b/app/components/Dialog/Dialog.tsx @@ -88,7 +88,7 @@ export function Dialog(props : DialogProps) { : - 0 ? avatars[0].avatar : undefined} variant={isInCurrentDialog ? 'filled' : 'light'} name={userInfo.title} size={50} color={'initials'} /> + 0 ? '#fff' : undefined} src={avatars.length > 0 ? avatars[0].avatar : undefined} variant={isInCurrentDialog ? 'filled' : 'light'} name={userInfo.title} size={50} color={'initials'} /> {userInfo.online == OnlineState.ONLINE && ( - {(!props.avatar_no_render && (md || !props.replyed)) && 0 ? avatars[0].avatar : undefined} name={user.title} variant={props.parent ? 'filled' : 'light'} color="initials">} + {(!props.avatar_no_render && (md || !props.replyed)) && 0 ? '#fff' : undefined} onClick={navigateToUserProfile} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={user.title} variant={props.parent ? 'filled' : 'light'} color="initials">} {!props.avatar_no_render && ( @@ -262,7 +262,7 @@ export function Message(props: MessageProps) { return ( {(md && props.is_last_message_in_stack) && ( - 0 ? avatars[0].avatar : undefined} name={user.title} color="initials" variant={wallpaper != '' ? 'filled' : 'light'} style={{ flexShrink: 0 }}> + 0 ? '#fff' : undefined} onClick={navigateToUserProfile} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={user.title} color="initials" variant={wallpaper != '' ? 'filled' : 'light'} style={{ flexShrink: 0 }}> )} {(md && !props.is_last_message_in_stack) && ( diff --git a/app/providers/ImageViewerProvider/ImageViewer.tsx b/app/providers/ImageViewerProvider/ImageViewer.tsx index b4b5931..9454fd0 100644 --- a/app/providers/ImageViewerProvider/ImageViewer.tsx +++ b/app/providers/ImageViewerProvider/ImageViewer.tsx @@ -149,6 +149,7 @@ export function ImageViewer(props : ImageViewerProps) { userSelect: 'none', cursor: isDragging ? 'grabbing' : 'grab', transformOrigin: '0 0', + background: '#FFF', transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`, }} onWheel={onWheel} diff --git a/app/workers/crypto/crypto.worker.ts b/app/workers/crypto/crypto.worker.ts index 6765fdf..12d1dcf 100644 --- a/app/workers/crypto/crypto.worker.ts +++ b/app/workers/crypto/crypto.worker.ts @@ -21,7 +21,6 @@ self.onmessage = async (event: MessageEvent) => { result = await encrypt(payload.data, payload.publicKey); break; case 'decrypt': - console.info("decrypt", payload.privateKey, payload.data); result = await decrypt(payload.data, payload.privateKey); break; case 'chacha20Encrypt': From 84d3cc7be41858d3da32be6ad272611c17d47311 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Feb 2026 12:20:35 +0200 Subject: [PATCH 18/67] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B7=D1=80=D0=B0?= =?UTF-8?q?=D1=87=D0=BD=D1=8B=D0=BC=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=BB=D0=BE=D0=B6=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ChatHeader/ChatHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/ChatHeader/ChatHeader.tsx b/app/components/ChatHeader/ChatHeader.tsx index 0b13363..4b15637 100644 --- a/app/components/ChatHeader/ChatHeader.tsx +++ b/app/components/ChatHeader/ChatHeader.tsx @@ -116,7 +116,7 @@ export function ChatHeader() { onClick={onClickProfile} > - : 0 ? avatars[0].avatar : undefined} name={opponent.title}> + : 0 ? '#fff' : undefined} color={'initials'} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={opponent.title}> } From b9603462a0d7e68bcbf7cc8c287f5e3e690eedc9 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Feb 2026 12:40:19 +0200 Subject: [PATCH 19/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=B4=20=D0=BF=D1=80=D0=BE=D0=B7?= =?UTF-8?q?=D1=80=D0=B0=D1=87=D0=BD=D1=8B=D0=B5=20=D0=B0=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/MentionList/MentionRow.tsx | 1 + app/components/UserRow/UserRow.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/app/components/MentionList/MentionRow.tsx b/app/components/MentionList/MentionRow.tsx index 742be56..922dbf7 100644 --- a/app/components/MentionList/MentionRow.tsx +++ b/app/components/MentionList/MentionRow.tsx @@ -22,6 +22,7 @@ export function MentionRow(props : MentionRowProps) { name={props.title} variant="light" color="initials" + bg={avatars.length > 0 ? '#fff' : undefined} src={avatars.length > 0 ? avatars[0].avatar : null} >} diff --git a/app/components/UserRow/UserRow.tsx b/app/components/UserRow/UserRow.tsx index dec80b8..585679b 100644 --- a/app/components/UserRow/UserRow.tsx +++ b/app/components/UserRow/UserRow.tsx @@ -40,6 +40,7 @@ export function UserRow(props: UserRowProps) { radius="xl" name={userInfo.title} color={'initials'} + bg={avatars.length > 0 ? '#fff' : undefined} src={avatars.length > 0 ? avatars[0].avatar : undefined} /> From c3a53b517e89468da63cb68db3172ff0fe381139 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Feb 2026 20:54:52 +0200 Subject: [PATCH 20/67] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BA=D0=B8=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/DialogProvider/DialogProvider.tsx | 11 ++++++++--- app/providers/DialogProvider/useDialogFiber.ts | 6 ++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 377e27d..f255ea2 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -295,7 +295,6 @@ export function DialogProvider(props: DialogProviderProps) { * Обработчик чтения для личных сообщений */ usePacket(0x07, async (packet : PacketRead) => { - info("Read packet received in dialog provider"); const fromPublicKey = packet.getFromPublicKey(); if(fromPublicKey == publicKey){ /** @@ -309,7 +308,10 @@ export function DialogProvider(props: DialogProviderProps) { */ return; } - if(fromPublicKey != props.dialog && !idle){ + if(idle){ + return; + } + if(fromPublicKey != props.dialog){ return; } setMessages((prev) => prev.map((msg) => { @@ -342,7 +344,10 @@ export function DialogProvider(props: DialogProviderProps) { */ return; } - if(toPublicKey != props.dialog && !idle){ + if(idle){ + return; + } + if(toPublicKey != props.dialog){ return; } setMessages((prev) => prev.map((msg) => { diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index c2399ce..ebcf58d 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -367,13 +367,11 @@ export function useDialogFiber() { return; } await updateSyncTime(Date.now()); - console.info("PACKED_READ_IM"); await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]); - console.info("read im with params ", [fromPublicKey, toPublicKey, publicKey]); updateDialog(fromPublicKey); - log("Read packet received from " + fromPublicKey + " for " + toPublicKey); + addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { - if (message.from_public_key == toPublicKey && !message.readed) { + if (message.from_public_key == publicKey && !message.readed) { console.info("Marking message as read in cache for dialog with " + fromPublicKey); console.info({ fromPublicKey, toPublicKey }); return { From 8b16c4ce0f571888525cf99243d24096a2835d23 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Feb 2026 20:55:55 +0200 Subject: [PATCH 21/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BA=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/MessageAttachments/MessageAvatar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/MessageAttachments/MessageAvatar.tsx b/app/components/MessageAttachments/MessageAvatar.tsx index 561cb21..53d19aa 100644 --- a/app/components/MessageAttachments/MessageAvatar.tsx +++ b/app/components/MessageAttachments/MessageAvatar.tsx @@ -78,7 +78,8 @@ export function MessageAvatar(props: AttachmentProps) { height: 60, width: 60, borderRadius: '50%', - objectFit: 'cover' + objectFit: 'cover', + background: '#fff' }} src={blob}>)} {downloadStatus != DownloadStatus.DOWNLOADED && downloadStatus != DownloadStatus.PENDING && preview.length >= 20 && ( <> From 461ccbfa9480886ecae5548c6374f34e4768f3b2 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 28 Feb 2026 12:48:53 +0200 Subject: [PATCH 22/67] =?UTF-8?q?=D0=94=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=20?= =?UTF-8?q?=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/App.tsx | 6 +- .../ActiveCall/ActiveCall.module.css | 42 +++++++ app/components/ActiveCall/ActiveCall.tsx | 95 +++++++++++++++ app/components/Call/Call.tsx | 111 ++++++++++++++++++ app/components/ChatHeader/ChatHeader.tsx | 58 +++------ app/components/DialogsPanel/DialogsPanel.tsx | 6 + .../MacFrameButtons.module.css | 2 +- app/providers/CallProvider/CallProvider.tsx | 70 +++++++++++ .../CallProvider/translateDurationTime.ts | 5 + app/providers/CallProvider/useCalls.ts | 15 +++ app/views/Chat/Chat.tsx | 6 +- app/views/Main/Main.tsx | 95 ++++++++------- package.json | 2 +- 13 files changed, 416 insertions(+), 97 deletions(-) create mode 100644 app/components/ActiveCall/ActiveCall.module.css create mode 100644 app/components/ActiveCall/ActiveCall.tsx create mode 100644 app/components/Call/Call.tsx create mode 100644 app/providers/CallProvider/CallProvider.tsx create mode 100644 app/providers/CallProvider/translateDurationTime.ts create mode 100644 app/providers/CallProvider/useCalls.ts diff --git a/app/App.tsx b/app/App.tsx index 5087969..5d0d7cf 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -6,9 +6,8 @@ import { ConfirmSeed } from './views/ConfirmSeed/ConfirmSeed'; import { SetPassword } from './views/SetPassword/SetPassword'; import { Main } from './views/Main/Main'; import { ExistsSeed } from './views/ExistsSeed/ExistsSeed'; -import { Box, Divider } from '@mantine/core'; +import { Box } from '@mantine/core'; import './style.css' -import { useRosettaColors } from './hooks/useRosettaColors'; import { Buffer } from 'buffer'; import { InformationProvider } from './providers/InformationProvider/InformationProvider'; import { BlacklistProvider } from './providers/BlacklistProvider/BlacklistProvider'; @@ -27,8 +26,6 @@ window.Buffer = Buffer; export default function App() { const { allAccounts, accountProviderLoaded } = useAccountProvider(); - const colors = useRosettaColors(); - const getViewByLoginState = () => { if (!accountProviderLoaded) { @@ -59,7 +56,6 @@ export default function App() { - diff --git a/app/components/ActiveCall/ActiveCall.module.css b/app/components/ActiveCall/ActiveCall.module.css new file mode 100644 index 0000000..51b0a41 --- /dev/null +++ b/app/components/ActiveCall/ActiveCall.module.css @@ -0,0 +1,42 @@ +.active { + background: linear-gradient(90deg,rgba(0, 186, 59, 1) 0%, rgba(0, 194, 81, 1) 50%); + background-size: 200% 200%; + animation: activeFlow 5s ease-in-out infinite; +} + +@keyframes activeFlow { + 0% { + background-position: 0% 50%; + filter: saturate(1); + } + 50% { + background-position: 100% 50%; + filter: saturate(1.15); + } + 100% { + background-position: 0% 50%; + filter: saturate(1); + } +} + +.connecting { + background: linear-gradient(120deg, #ff2d2d, #ff7a00, #ff2d2d); + background-size: 220% 220%; + animation: connectingFlow 5s ease-in-out infinite; +} + +@keyframes connectingFlow { + 0% { + background-position: 0% 50%; + filter: saturate(1); + } + 50% { + background-position: 100% 50%; + filter: saturate(1.15); + } + 100% { + background-position: 0% 50%; + filter: saturate(1); + } +} +/* ...existing code... */ \ No newline at end of file diff --git a/app/components/ActiveCall/ActiveCall.tsx b/app/components/ActiveCall/ActiveCall.tsx new file mode 100644 index 0000000..ed7f027 --- /dev/null +++ b/app/components/ActiveCall/ActiveCall.tsx @@ -0,0 +1,95 @@ +import { useCalls } from "@/app/providers/CallProvider/useCalls"; +import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation"; +import { Box, Flex, Loader, Text } from "@mantine/core"; +import classes from "./ActiveCall.module.css"; +import { CallState } from "@/app/providers/CallProvider/CallProvider"; +import { IconMicrophone, IconMicrophoneOff, IconPhoneX, IconVolume, IconVolumeOff } from "@tabler/icons-react"; +import { translateDurationToTime } from "@/app/providers/CallProvider/translateDurationTime"; + +export function ActiveCall() { + const {activeCall, callState, duration, muted, sound, close, setMuted, setSound, setShowCallView} = useCalls(); + const [userInfo] = useUserInformation(activeCall); + //const colors = useRosettaColors(); + + if(activeCall == ""){ + return <> + } + + const getConnectingClass = () => { + if(callState === CallState.CONNECTING){ + return classes.connecting; + } + if(callState === CallState.ACTIVE){ + return classes.active; + } + return ""; + } + + return ( + <> + setShowCallView(true)}> + + + + {!muted && ( + { + e.stopPropagation(); + setMuted(true); + }} size={16} color={'#fff'}> + )} + {muted && ( + { + e.stopPropagation(); + setMuted(false); + }} size={16} color={'#fff'}> + )} + + + {userInfo?.title || activeCall} + {callState === CallState.CONNECTING && ( + + )} + {callState == CallState.ACTIVE && ( + {translateDurationToTime(duration)} + )} + + + {sound && ( + { + e.stopPropagation(); + setSound(false) + }} color={'#fff'}> + )} + {!sound && ( + { + e.stopPropagation(); + setSound(true) + }} color={'#fff'}> + )} + { + e.stopPropagation(); + close(); + }} size={16} color={'#fff'}> + + + + + + ); +} \ No newline at end of file diff --git a/app/components/Call/Call.tsx b/app/components/Call/Call.tsx new file mode 100644 index 0000000..aa63880 --- /dev/null +++ b/app/components/Call/Call.tsx @@ -0,0 +1,111 @@ +import { useRosettaColors } from "@/app/hooks/useRosettaColors"; +import { useAvatars } from "@/app/providers/AvatarProvider/useAvatars"; +import { CallContextValue, CallState } from "@/app/providers/CallProvider/CallProvider"; +import { translateDurationToTime } from "@/app/providers/CallProvider/translateDurationTime"; +import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation"; +import { Avatar, Box, Flex, Text } from "@mantine/core"; +import { IconChevronLeft, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneX, IconQrcode, IconVolume, IconVolumeOff, IconX } from "@tabler/icons-react"; + +export interface CallProps { + context: CallContextValue; +} + +export function Call(props: CallProps) { + const { + activeCall, + duration, + callState, + close, + sound, + setSound, + setMuted, + setShowCallView, + muted} = props.context; + const [userInfo] = useUserInformation(activeCall); + const avatars = useAvatars(activeCall); + const colors = useRosettaColors(); + + return ( + + + + setShowCallView(false)} justify={'center'} align={'center'}> + + Back + + + + + + + 0 ? '#fff' : undefined} src={avatars.length > 0 ? avatars[0].avatar : undefined} color={'initials'} name={userInfo.title}> + {userInfo.title} + {callState == CallState.ACTIVE && ({translateDurationToTime(duration)})} + {callState == CallState.CONNECTING && (Connecting...)} + {callState == CallState.INCOMING && (Incoming call...)} + + {callState == CallState.ACTIVE || callState == CallState.CONNECTING && ( + <> + setSound(!sound)} style={{ + borderRadius: 25, + cursor: 'pointer' + }} h={50} bg={sound ? colors.chevrons.active : colors.chevrons.disabled}> + + {!sound && } + {sound && } + + + setMuted(!muted)} style={{ + borderRadius: 25, + cursor: 'pointer' + }} h={50} bg={!muted ? colors.chevrons.active : colors.chevrons.disabled}> + + {muted && } + {!muted && } + + + + + + + + + )} + {callState == CallState.INCOMING && ( + <> + {userInfo.title != "Rosetta" && ( + + + + + + )} + + + + + + + )} + + + + + ) +} \ No newline at end of file diff --git a/app/components/ChatHeader/ChatHeader.tsx b/app/components/ChatHeader/ChatHeader.tsx index 4b15637..f14023e 100644 --- a/app/components/ChatHeader/ChatHeader.tsx +++ b/app/components/ChatHeader/ChatHeader.tsx @@ -1,14 +1,13 @@ import { useRosettaColors } from "@/app/hooks/useRosettaColors"; import { OnlineState } from "@/app/providers/ProtocolProvider/protocol/packets/packet.onlinestate"; import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey"; -import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist"; import { useDialog } from "@/app/providers/DialogProvider/useDialog"; import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation"; import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider"; import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState"; -import { Avatar, Box, Divider, Flex, Loader, Text, Tooltip, useComputedColorScheme, useMantineTheme } from "@mantine/core"; +import { Avatar, Box, Divider, Flex, Loader, Text, useComputedColorScheme, useMantineTheme } from "@mantine/core"; import { modals } from "@mantine/modals"; -import { IconBookmark, IconLockAccess, IconLockCancel, IconTrashX } from "@tabler/icons-react"; +import { IconBookmark, IconPhone, IconTrashX } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { VerifiedBadge } from "../VerifiedBadge/VerifiedBadge"; @@ -20,6 +19,7 @@ import { ReplyHeader } from "../ReplyHeader/ReplyHeader"; import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints"; import { BackToDialogs } from "../BackToDialogs/BackToDialogs"; import { useSystemAccounts } from "@/app/providers/SystemAccountsProvider/useSystemAccounts"; +import { useCalls } from "@/app/providers/CallProvider/useCalls"; export function ChatHeader() { @@ -29,7 +29,6 @@ export function ChatHeader() { const publicKey = usePublicKey(); const {deleteMessages, dialog} = useDialog(); const theme = useMantineTheme(); - const [blocked, blockUser, unblockUser] = useBlacklist(dialog); const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog); const [protocolState] = useProtocolState(); const [userTypeing, setUserTypeing] = useState(false); @@ -39,6 +38,7 @@ export function ChatHeader() { const {lg} = useRosettaBreakpoints(); const systemAccounts = useSystemAccounts(); const isSystemAccount = systemAccounts.find((acc) => acc.publicKey == dialog) != undefined; + const {call} = useCalls(); useEffect(() => { @@ -78,20 +78,6 @@ export function ChatHeader() { }); } - const onClickBlockUser = () => { - if(opponent.publicKey != "DELETED" - && opponent.publicKey != publicKey){ - blockUser(); - } - } - - const onClickUnblockUser = () => { - if(opponent.publicKey != "DELETED" - && opponent.publicKey != publicKey){ - unblockUser(); - } - } - const onClickProfile = () => { if(opponent.publicKey != "DELETED" && opponent.publicKey != publicKey){ navigate("/main/profile/" + opponent.publicKey); @@ -149,32 +135,16 @@ export function ChatHeader() { - - - - {publicKey != opponent.publicKey && !blocked && !isSystemAccount && ( - - - - - )} - {blocked && !isSystemAccount && ( - - - - - )} + call(dialog)} + style={{ + cursor: 'pointer' + }} stroke={1.5} color={theme.colors.blue[7]} size={24}> + } {replyMessages.messages.length > 0 && !replyMessages.inDialogInput && } diff --git a/app/components/DialogsPanel/DialogsPanel.tsx b/app/components/DialogsPanel/DialogsPanel.tsx index f4bde5e..8c2b8b1 100644 --- a/app/components/DialogsPanel/DialogsPanel.tsx +++ b/app/components/DialogsPanel/DialogsPanel.tsx @@ -10,6 +10,8 @@ import { DialogsPanelHeader } from '../DialogsPanelHeader/DialogsPanelHeader'; import { useDialogsList } from '@/app/providers/DialogListProvider/useDialogsList'; import { useVerifyRequest } from '@/app/providers/DeviceProvider/useVerifyRequest'; import { DeviceVerify } from '../DeviceVerify/DeviceVerify'; +import { ActiveCall } from '../ActiveCall/ActiveCall'; +import { useViewPanelsState, ViewPanelsState } from '@/app/hooks/useViewPanelsState'; export function DialogsPanel() { const [dialogsMode, setDialogsMode] = useState<'all' | 'requests'>('all'); @@ -18,6 +20,7 @@ export function DialogsPanel() { const colors = useRosettaColors(); const navigate = useNavigate(); const device = useVerifyRequest(); + const [viewState] = useViewPanelsState(); useEffect(() => { ((async () => { @@ -52,6 +55,9 @@ export function DialogsPanel() { direction={'column'} justify={'space-between'} > + {viewState == ViewPanelsState.DIALOGS_PANEL_ONLY && ( + + )} {device && ( diff --git a/app/components/MacFrameButtons/MacFrameButtons.module.css b/app/components/MacFrameButtons/MacFrameButtons.module.css index b00a813..ef4bb89 100644 --- a/app/components/MacFrameButtons/MacFrameButtons.module.css +++ b/app/components/MacFrameButtons/MacFrameButtons.module.css @@ -4,7 +4,7 @@ left: 12px; display: flex; gap: 8px; - z-index: 10; + z-index: 15; app-region: no-drag; } .close_btn, .minimize_btn, .maximize_btn { diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx new file mode 100644 index 0000000..e532c14 --- /dev/null +++ b/app/providers/CallProvider/CallProvider.tsx @@ -0,0 +1,70 @@ +import { Call } from "@/app/components/Call/Call"; +import { createContext, useState } from "react"; + + +export interface CallContextValue { + call: (callable: string) => void; + close: () => void; + activeCall: string; + callState: CallState; + muted: boolean; + sound: boolean; + setMuted: (muted: boolean) => void; + setSound: (sound: boolean) => void; + duration: number; + setShowCallView: (show: boolean) => void; +} + +export enum CallState { + CONNECTING, + ACTIVE, + ENDED, + INCOMING +} + +export const CallContext = createContext(null); +export interface CallProviderProps { + children: React.ReactNode; +} + +export function CallProvider(props : CallProviderProps) { + const [activeCall, setActiveCall] = useState(""); + const [callState, setCallState] = useState(CallState.ENDED); + const [muted, setMuted] = useState(false); + const [sound, setSound] = useState(true); + const [duration, setDuration] = useState(0); + const [showCallView, setShowCallView] = useState(callState == CallState.INCOMING); + + const call = (dialog: string) => { + setActiveCall(dialog); + setCallState(CallState.CONNECTING); + setShowCallView(true); + } + + const close = () => { + setActiveCall(""); + setCallState(CallState.ENDED); + setShowCallView(false); + setDuration(0); + } + + const context = { + call, + close, + activeCall, + callState, + muted, + sound, + setMuted, + setSound, + duration, + setShowCallView + }; + + return ( + + {props.children} + {showCallView && } + + ) +} \ No newline at end of file diff --git a/app/providers/CallProvider/translateDurationTime.ts b/app/providers/CallProvider/translateDurationTime.ts new file mode 100644 index 0000000..fe27634 --- /dev/null +++ b/app/providers/CallProvider/translateDurationTime.ts @@ -0,0 +1,5 @@ +export const translateDurationToTime = (duration: number) => { + const minutes = Math.floor(duration / 60); + const seconds = duration % 60; + return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; +} \ No newline at end of file diff --git a/app/providers/CallProvider/useCalls.ts b/app/providers/CallProvider/useCalls.ts new file mode 100644 index 0000000..45e3aea --- /dev/null +++ b/app/providers/CallProvider/useCalls.ts @@ -0,0 +1,15 @@ +import { useContext } from "react"; +import { CallContext, CallContextValue } from "./CallProvider"; + +/** + * Хук предоставляет функции для работы с звонками, такие как инициирование звонка, принятие звонка, завершение звонка и т.д. + * Он может использоваться в компонентах, связанных с звонками, для управления состоянием звонков и взаимодействия с сервером. + */ +export function useCalls() : CallContextValue { + const context = useContext(CallContext); + if (!context) { + throw new Error("useCalls must be used within a CallProvider"); + } + + return context; +} \ No newline at end of file diff --git a/app/views/Chat/Chat.tsx b/app/views/Chat/Chat.tsx index 5196d8e..6e393f5 100644 --- a/app/views/Chat/Chat.tsx +++ b/app/views/Chat/Chat.tsx @@ -9,12 +9,13 @@ import { useEffect } from "react"; import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState"; import { GroupHeader } from "@/app/components/GroupHeader/GroupHeader"; import { useGroups } from "@/app/providers/DialogProvider/useGroups"; +import { ActiveCall } from "@/app/components/ActiveCall/ActiveCall"; export function Chat() { const params = useParams(); const dialog = params.id || "DELETED"; const {lg} = useRosettaBreakpoints(); - const [__, setViewState] = useViewPanelsState(); + const [viewState, setViewState] = useViewPanelsState(); const {hasGroup} = useGroups(); useEffect(() => { @@ -30,6 +31,9 @@ export function Chat() { return (<> + {viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && ( + + )} {/* Group Header */} {hasGroup(dialog) && } {/* Dialog peer to peer Header */} diff --git a/app/views/Main/Main.tsx b/app/views/Main/Main.tsx index fdb45d1..847bb6d 100644 --- a/app/views/Main/Main.tsx +++ b/app/views/Main/Main.tsx @@ -31,6 +31,7 @@ import { useUpdateMessage } from "@/app/hooks/useUpdateMessage"; import { useDeviceMessage } from "@/app/hooks/useDeviceMessage"; import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider"; import { useSynchronize } from "@/app/providers/DialogProvider/useSynchronize"; +import { CallProvider } from "@/app/providers/CallProvider/CallProvider"; export function Main() { const { mainColor, borderColor } = useRosettaColors(); @@ -154,52 +155,56 @@ export function Main() { - -
+ - -
- - {viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && - - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - - } -
- {oldPublicKey && ( - - - - Your account uses an old format public key which is no longer supported. Please create a new account to continue using the application. -

After press "OK" button, the application will close and remove all data. -
- -
-
- )} +
+ +
+ {lg && ( + + )} + {viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && + + }> + }> + }> + }> + }> + }> + }> + }> + }> + }> + }> + + } +
+ {oldPublicKey && ( + + + + Your account uses an old format public key which is no longer supported. Please create a new account to continue using the application. +

After press "OK" button, the application will close and remove all data. +
+ +
+
+ )} + diff --git a/package.json b/package.json index 153dc92..4c591dc 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "@electron-toolkit/eslint-config": "^2.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/tsconfig": "^1.0.1", + "@electron/rebuild": "^4.0.3", "@rushstack/eslint-patch": "^1.10.5", "@tailwindcss/vite": "^4.0.9", "@types/node": "^22.13.5", @@ -132,7 +133,6 @@ "@vitejs/plugin-react": "^4.3.4", "electron": "^38.3.0", "electron-builder": "^25.1.8", - "@electron/rebuild": "^4.0.3", "electron-vite": "^3.0.0", "eslint": "^9.21.0", "eslint-plugin-react": "^7.37.4", From 9eac2fae6fa5dceadffec76412da2ba47e0f7610 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 28 Feb 2026 17:33:23 +0200 Subject: [PATCH 23/67] =?UTF-8?q?=D0=9E=D0=B1=D0=BC=D0=B5=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D0=BC=D0=B8=20=D1=88=D0=B8=D1=84=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20DH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/Call/Call.tsx | 34 +++- app/dev.html | 3 + app/providers/CallProvider/CallProvider.tsx | 150 +++++++++++++++++- .../protocol/packets/packet.signal.ts | 83 ++++++++++ .../ProtocolProvider/protocol/protocol.ts | 2 + package.json | 7 +- 6 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 app/providers/ProtocolProvider/protocol/packets/packet.signal.ts diff --git a/app/components/Call/Call.tsx b/app/components/Call/Call.tsx index aa63880..7538217 100644 --- a/app/components/Call/Call.tsx +++ b/app/components/Call/Call.tsx @@ -3,8 +3,9 @@ import { useAvatars } from "@/app/providers/AvatarProvider/useAvatars"; import { CallContextValue, CallState } from "@/app/providers/CallProvider/CallProvider"; import { translateDurationToTime } from "@/app/providers/CallProvider/translateDurationTime"; import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation"; -import { Avatar, Box, Flex, Text } from "@mantine/core"; +import { Avatar, Box, Flex, Popover, Text, useMantineTheme } from "@mantine/core"; import { IconChevronLeft, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneX, IconQrcode, IconVolume, IconVolumeOff, IconX } from "@tabler/icons-react"; +import { KeyImage } from "../KeyImage/KeyImage"; export interface CallProps { context: CallContextValue; @@ -20,10 +21,14 @@ export function Call(props: CallProps) { setSound, setMuted, setShowCallView, - muted} = props.context; + muted, + getKeyCast, + accept + } = props.context; const [userInfo] = useUserInformation(activeCall); const avatars = useAvatars(activeCall); const colors = useRosettaColors(); + const theme = useMantineTheme(); return ( Back
- + + + + + + + + This call is secured by 256 bit end-to-end encryption. Only you and the recipient can read or listen to the content of this call. + + + + + {translateDurationToTime(duration)})} {callState == CallState.CONNECTING && (Connecting...)} {callState == CallState.INCOMING && (Incoming call...)} + {callState == CallState.KEY_EXCHANGE && (Exchanging encryption keys...)} - {callState == CallState.ACTIVE || callState == CallState.CONNECTING && ( + {callState == CallState.ACTIVE || callState == CallState.CONNECTING || callState == CallState.KEY_EXCHANGE && ( <> setSound(!sound)} style={{ borderRadius: 25, @@ -93,7 +117,7 @@ export function Call(props: CallProps) {
)} - diff --git a/app/dev.html b/app/dev.html index 969a459..dd03527 100644 --- a/app/dev.html +++ b/app/dev.html @@ -6,6 +6,9 @@
+ diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index e532c14..57e3743 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -1,5 +1,11 @@ import { Call } from "@/app/components/Call/Call"; -import { createContext, useState } from "react"; +import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; +import { createContext, useEffect, useRef, useState } from "react"; +import nacl from 'tweetnacl'; +import { useSender } from "../ProtocolProvider/useSender"; +import { PacketSignal, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal"; +import { usePacket } from "../ProtocolProvider/usePacket"; +import { usePublicKey } from "../AccountProvider/usePublicKey"; export interface CallContextValue { @@ -13,15 +19,29 @@ export interface CallContextValue { setSound: (sound: boolean) => void; duration: number; setShowCallView: (show: boolean) => void; + getKeyCast: () => string; + accept: () => void; } export enum CallState { CONNECTING, + KEY_EXCHANGE, ACTIVE, ENDED, INCOMING } +export enum CallRole { + /** + * Вызывающая сторона, которая инициирует звонок + */ + CALLER, + /** + * Принимающая сторона, которая отвечает на звонок и принимает его + */ + CALLEE +} + export const CallContext = createContext(null); export interface CallProviderProps { children: React.ReactNode; @@ -34,18 +54,142 @@ export function CallProvider(props : CallProviderProps) { const [sound, setSound] = useState(true); const [duration, setDuration] = useState(0); const [showCallView, setShowCallView] = useState(callState == CallState.INCOMING); + const {info} = useConsoleLogger("CallProvider"); + const [sessionKeys, setSessionKeys] = useState(null); + const send = useSender(); + const publicKey = usePublicKey(); + + const roleRef = useRef(null); + const [sharedSecret, setSharedSecret] = useState(""); + + useEffect(() => { + console.info("TRACE -> ", sharedSecret) + }, [sharedSecret]); + + usePacket(26, (packet: PacketSignal) => { + const signalType = packet.getSignalType(); + if(activeCall){ + /** + * У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка + */ + if(packet.getSrc() != activeCall){ + info("Received signal for another call, ignoring"); + return; + } + } + if(signalType == SignalType.CALL){ + /** + * Нам поступает звонок + */ + setActiveCall(packet.getSrc()); + setCallState(CallState.INCOMING); + setShowCallView(true); + } + if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){ + /** + * Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть, + * теперь мы можем создать общую секретную сессию для шифрования звонка + */ + const sharedPublic = packet.getSharedPublic(); + if(!sharedPublic){ + info("Received key exchange signal without shared public key"); + return; + } + const sessionKeys = generateSessionKeys(); + const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); + setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); + info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); + /** + * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию + */ + const signalPacket = new PacketSignal(); + signalPacket.setSrc(publicKey); + signalPacket.setDst(packet.getDst()); + signalPacket.setSignalType(SignalType.KEY_EXCHANGE); + signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex')); + send(signalPacket); + } + if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){ + /** + * Мы отправили свою публичную часть ключа другой стороне, + * теперь мы получили ее публичную часть и можем создать общую + * секретную сессию для шифрования звонка + */ + const sharedPublic = packet.getSharedPublic(); + if(!sharedPublic){ + info("Received key exchange signal without shared public key"); + return; + } + if(!sessionKeys){ + info("Received key exchange signal but session keys are not generated"); + return; + } + const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); + info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); + setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); + } + if(signalType == SignalType.ACTIVE_CALL) { + setCallState(CallState.ACTIVE); + } + }, []); + + const generateSessionKeys = () => { + const sessionKeys = nacl.box.keyPair(); + info("Generated keys for call session, len: " + sessionKeys.publicKey.length); + setSessionKeys(sessionKeys); + return sessionKeys; + } const call = (dialog: string) => { setActiveCall(dialog); setCallState(CallState.CONNECTING); setShowCallView(true); + const signalPacket = new PacketSignal(); + signalPacket.setDst(dialog); + signalPacket.setSignalType(SignalType.CALL); + send(signalPacket); + roleRef.current = CallRole.CALLER; } const close = () => { setActiveCall(""); setCallState(CallState.ENDED); setShowCallView(false); + setSessionKeys(null); setDuration(0); + roleRef.current = null; + } + + const accept = () => { + if(callState != CallState.INCOMING){ + /** + * Нечего принимать + */ + return; + } + /** + * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи + */ + const keys = generateSessionKeys(); + const signalPacket = new PacketSignal(); + signalPacket.setDst(activeCall); + signalPacket.setSignalType(SignalType.KEY_EXCHANGE); + signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex')); + send(signalPacket); + setCallState(CallState.KEY_EXCHANGE); + roleRef.current = CallRole.CALLEE; + } + + /** + * Получает слепок ключа для отображения в UI + * чтобы не показывать настоящий ключ + * @returns + */ + const getKeyCast = () => { + if(!sessionKeys){ + return ""; + } + return Buffer.from(sessionKeys.secretKey).toString('hex'); } const context = { @@ -58,7 +202,9 @@ export function CallProvider(props : CallProviderProps) { setMuted, setSound, duration, - setShowCallView + setShowCallView, + getKeyCast, + accept }; return ( diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts new file mode 100644 index 0000000..5a6ce5f --- /dev/null +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts @@ -0,0 +1,83 @@ +import Packet from "../packet"; +import Stream from "../stream"; + +export enum SignalType { + CALL = 1, + KEY_EXCHANGE = 2, + ACTIVE_CALL = 3, + END_CALL = 4 +} + +export class PacketSignal extends Packet { + + private src: string = ""; + /** + * Назначение + */ + private dst: string = ""; + /** + * Используется если SignalType == KEY_EXCHANGE, для идентификации сессии обмена ключами + */ + private sharedPublic: string = ""; + + private signalType: SignalType = SignalType.CALL; + + + public getPacketId(): number { + return 26; + } + + public _receive(stream: Stream): void { + this.signalType = stream.readInt8(); + this.src = stream.readString(); + this.dst = stream.readString(); + if(this.signalType == SignalType.KEY_EXCHANGE){ + this.sharedPublic = stream.readString(); + } + } + + public _send(): Promise | Stream { + const stream = new Stream(); + stream.writeInt16(this.getPacketId()); + stream.writeInt8(this.signalType); + stream.writeString(this.src); + stream.writeString(this.dst); + if(this.signalType == SignalType.KEY_EXCHANGE){ + stream.writeString(this.sharedPublic); + } + return stream; + } + + public setDst(dst: string) { + this.dst = dst; + } + + public setSharedPublic(sharedPublic: string) { + this.sharedPublic = sharedPublic; + } + + public setSignalType(signalType: SignalType) { + this.signalType = signalType; + } + + public getDst(): string { + return this.dst; + } + + public getSharedPublic(): string { + return this.sharedPublic; + } + + public getSignalType(): SignalType { + return this.signalType; + } + + public getSrc(): string { + return this.src; + } + + public setSrc(src: string) { + this.src = src; + } + +} \ No newline at end of file diff --git a/app/providers/ProtocolProvider/protocol/protocol.ts b/app/providers/ProtocolProvider/protocol/protocol.ts index 2189a6a..90d2fe0 100644 --- a/app/providers/ProtocolProvider/protocol/protocol.ts +++ b/app/providers/ProtocolProvider/protocol/protocol.ts @@ -25,6 +25,7 @@ import { PacketDeviceNew } from "./packets/packet.device.new"; import { PacketDeviceList } from "./packets/packet.device.list"; import { PacketDeviceResolve } from "./packets/packet.device.resolve"; import { PacketSync } from "./packets/packet.sync"; +import { PacketSignal } from "./packets/packet.signal"; export default class Protocol extends EventEmitter { private serverAddress: string; @@ -125,6 +126,7 @@ export default class Protocol extends EventEmitter { this._supportedPackets.set(0x17, new PacketDeviceList()); this._supportedPackets.set(0x18, new PacketDeviceResolve()); this._supportedPackets.set(25, new PacketSync()); + this._supportedPackets.set(26, new PacketSignal()); } private _findWaiters(packetId: number): ((packet: Packet) => void)[] { diff --git a/package.json b/package.json index 4c591dc..16e87de 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,8 @@ "@noble/ciphers": "^1.2.1", "@noble/secp256k1": "^3.0.0", "@tabler/icons-react": "^3.31.0", + "@types/crypto-js": "^4.2.2", + "@types/diffie-hellman": "^5.0.3", "@types/elliptic": "^6.4.18", "@types/node-forge": "^1.3.11", "@types/npm": "^7.19.3", @@ -90,7 +92,6 @@ "bip39": "^3.1.0", "blurhash": "^2.0.5", "buffer": "^6.0.3", - "crypto-browserify": "^3.12.1", "crypto-js": "^4.2.0", "dayjs": "^1.11.13", "elliptic": "^6.6.1", @@ -103,9 +104,11 @@ "i": "^0.3.7", "jsencrypt": "^3.3.2", "jszip": "^3.10.1", + "libsodium": "^0.8.2", "lottie-react": "^2.4.1", "node-forge": "^1.3.1", "node-machine-id": "^1.1.12", + "npm": "^11.11.0", "pako": "^2.1.0", "react-router-dom": "^7.4.0", "react-syntax-highlighter": "^16.1.0", @@ -115,6 +118,8 @@ "recharts": "^2.15.1", "sql.js": "^1.13.0", "sqlite3": "^5.1.7", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1", "wa-sqlite": "^1.0.0", "web-bip39": "^0.0.3" }, From 9ad0e5d00a36ac5a963736b5b87b296c84518289 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 28 Feb 2026 17:42:10 +0200 Subject: [PATCH 24/67] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B5=20SignalType?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProtocolProvider/protocol/packets/packet.signal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts index 5a6ce5f..d0a0d89 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts @@ -2,10 +2,10 @@ import Packet from "../packet"; import Stream from "../stream"; export enum SignalType { - CALL = 1, - KEY_EXCHANGE = 2, - ACTIVE_CALL = 3, - END_CALL = 4 + CALL = 0, + KEY_EXCHANGE = 1, + ACTIVE_CALL = 2, + END_CALL = 3 } export class PacketSignal extends Packet { From 7a89a3a307e468726ca548907ac1d1cab7644ca2 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 28 Feb 2026 18:31:21 +0200 Subject: [PATCH 25/67] OFFERS & ANSWERS webRTC --- app/providers/CallProvider/CallProvider.tsx | 25 ++++++++++++++++--- .../protocol/packets/packet.signal.ts | 5 +++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 57e3743..a8e4d4a 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -128,10 +128,29 @@ export function CallProvider(props : CallProviderProps) { info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); } + if(signalType == SignalType.OFFER){ + const offerJson = packet.getOffer(); + if(!offerJson || !peerConnectionRef.current){ + info("Received offer but peer connection is not ready"); + return; + } + + handleOffer(offerJson); + } + + if(signalType == SignalType.ANSWER){ + const answerJson = packet.getAnswer(); + if(!answerJson || !peerConnectionRef.current){ + info("Received answer but peer connection is not ready"); + return; + } + + handleAnswer(answerJson); + } if(signalType == SignalType.ACTIVE_CALL) { setCallState(CallState.ACTIVE); } - }, []); + }, [activeCall, sessionKeys]); const generateSessionKeys = () => { const sessionKeys = nacl.box.keyPair(); @@ -186,10 +205,10 @@ export function CallProvider(props : CallProviderProps) { * @returns */ const getKeyCast = () => { - if(!sessionKeys){ + if(!sharedSecret){ return ""; } - return Buffer.from(sessionKeys.secretKey).toString('hex'); + return sharedSecret; } const context = { diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts index d0a0d89..9d27d21 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts @@ -5,7 +5,10 @@ export enum SignalType { CALL = 0, KEY_EXCHANGE = 1, ACTIVE_CALL = 2, - END_CALL = 3 + END_CALL = 3, + OFFER = 4, + ANSWER = 5, + ICE_CONFIG = 6 } export class PacketSignal extends Packet { From e06d58facf36cc474433a9fa9f5f210f32dc6bd9 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Mon, 2 Mar 2026 18:53:15 +0200 Subject: [PATCH 26/67] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=D0=B8=D0=B3=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=D0=B0=20=D0=B8=20=D0=BE=D0=B1=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/Call/Call.tsx | 6 ++++- app/providers/CallProvider/CallProvider.tsx | 23 +------------------ .../protocol/packets/packet.signal.ts | 8 +++---- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/app/components/Call/Call.tsx b/app/components/Call/Call.tsx index 7538217..0b8b8b1 100644 --- a/app/components/Call/Call.tsx +++ b/app/components/Call/Call.tsx @@ -74,8 +74,12 @@ export function Call(props: CallProps) { {callState == CallState.CONNECTING && (Connecting...)} {callState == CallState.INCOMING && (Incoming call...)} {callState == CallState.KEY_EXCHANGE && (Exchanging encryption keys...)} + {callState == CallState.WEB_RTC_EXCHANGE && (Exchanging encryption keys...)} - {callState == CallState.ACTIVE || callState == CallState.CONNECTING || callState == CallState.KEY_EXCHANGE && ( + {(callState == CallState.ACTIVE + || callState == CallState.WEB_RTC_EXCHANGE + || callState == CallState.CONNECTING + || callState == CallState.KEY_EXCHANGE) && ( <> setSound(!sound)} style={{ borderRadius: 25, diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index a8e4d4a..ff726de 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -26,6 +26,7 @@ export interface CallContextValue { export enum CallState { CONNECTING, KEY_EXCHANGE, + WEB_RTC_EXCHANGE, ACTIVE, ENDED, INCOMING @@ -128,28 +129,6 @@ export function CallProvider(props : CallProviderProps) { info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); } - if(signalType == SignalType.OFFER){ - const offerJson = packet.getOffer(); - if(!offerJson || !peerConnectionRef.current){ - info("Received offer but peer connection is not ready"); - return; - } - - handleOffer(offerJson); - } - - if(signalType == SignalType.ANSWER){ - const answerJson = packet.getAnswer(); - if(!answerJson || !peerConnectionRef.current){ - info("Received answer but peer connection is not ready"); - return; - } - - handleAnswer(answerJson); - } - if(signalType == SignalType.ACTIVE_CALL) { - setCallState(CallState.ACTIVE); - } }, [activeCall, sessionKeys]); const generateSessionKeys = () => { diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts index 9d27d21..b57dd9d 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts @@ -5,12 +5,12 @@ export enum SignalType { CALL = 0, KEY_EXCHANGE = 1, ACTIVE_CALL = 2, - END_CALL = 3, - OFFER = 4, - ANSWER = 5, - ICE_CONFIG = 6 + END_CALL = 3 } +/** + * Пакет сигналинга, для сигналов WebRTC используется отдельный пакет 27 PacketWebRTCExchange + */ export class PacketSignal extends Packet { private src: string = ""; From e79282755b49d5b1d4ad3c220427631ebb89067f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 11 Mar 2026 17:22:29 +0200 Subject: [PATCH 27/67] =?UTF-8?q?=D0=A4=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D0=BC=D0=B8=20=D1=88=D0=B8=D1=84=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F,=20=D0=B2=D1=81=D0=B5=20?= =?UTF-8?q?=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BE=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B8=20WebRTC?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 33 ++++++++++++++++++- .../protocol/packets/packet.signal.ts | 2 +- app/servers.ts | 6 ++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index ff726de..b153a51 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -26,6 +26,10 @@ export interface CallContextValue { export enum CallState { CONNECTING, KEY_EXCHANGE, + /** + * Финальная стадия сигналинга, на которой обе стороны обменялись ключами и теперь устанавливают защищенный канал связи для звонка, + * через WebRTC, и готовятся к активному звонку. + */ WEB_RTC_EXCHANGE, ACTIVE, ENDED, @@ -59,6 +63,7 @@ export function CallProvider(props : CallProviderProps) { const [sessionKeys, setSessionKeys] = useState(null); const send = useSender(); const publicKey = usePublicKey(); + const peerConnectionRef = useRef(null); const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); @@ -74,10 +79,24 @@ export function CallProvider(props : CallProviderProps) { * У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка */ if(packet.getSrc() != activeCall){ + console.info("Received signal from " + packet.getSrc() + " but active call is with " + activeCall + ", ignoring"); info("Received signal for another call, ignoring"); return; } } + if(signalType == SignalType.END_CALL){ + /** + * Сбросили звонок + */ + setActiveCall(""); + setCallState(CallState.ENDED); + setShowCallView(false); + setSessionKeys(null); + setSharedSecret(""); + setDuration(0); + roleRef.current = null; + return; + } if(signalType == SignalType.CALL){ /** * Нам поступает звонок @@ -87,6 +106,7 @@ export function CallProvider(props : CallProviderProps) { setShowCallView(true); } if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){ + console.info("EXCHANGE SIGNAL RECEIVED, CALLER ROLE"); /** * Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть, * теперь мы можем создать общую секретную сессию для шифрования звонка @@ -105,12 +125,14 @@ export function CallProvider(props : CallProviderProps) { */ const signalPacket = new PacketSignal(); signalPacket.setSrc(publicKey); - signalPacket.setDst(packet.getDst()); + signalPacket.setDst(packet.getSrc()); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex')); send(signalPacket); + setCallState(CallState.WEB_RTC_EXCHANGE); } if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){ + console.info("EXCHANGE SIGNAL RECEIVED, CALLEE ROLE"); /** * Мы отправили свою публичную часть ключа другой стороне, * теперь мы получили ее публичную часть и можем создать общую @@ -128,6 +150,7 @@ export function CallProvider(props : CallProviderProps) { const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); + setCallState(CallState.WEB_RTC_EXCHANGE); } }, [activeCall, sessionKeys]); @@ -143,6 +166,7 @@ export function CallProvider(props : CallProviderProps) { setCallState(CallState.CONNECTING); setShowCallView(true); const signalPacket = new PacketSignal(); + signalPacket.setSrc(publicKey); signalPacket.setDst(dialog); signalPacket.setSignalType(SignalType.CALL); send(signalPacket); @@ -150,6 +174,12 @@ export function CallProvider(props : CallProviderProps) { } const close = () => { + const packetSignal = new PacketSignal(); + packetSignal.setSrc(publicKey); + packetSignal.setDst(activeCall); + packetSignal.setSignalType(SignalType.END_CALL); + send(packetSignal); + peerConnectionRef.current = null; setActiveCall(""); setCallState(CallState.ENDED); setShowCallView(false); @@ -170,6 +200,7 @@ export function CallProvider(props : CallProviderProps) { */ const keys = generateSessionKeys(); const signalPacket = new PacketSignal(); + signalPacket.setSrc(publicKey); signalPacket.setDst(activeCall); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex')); diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts index b57dd9d..54c86b2 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts @@ -5,7 +5,7 @@ export enum SignalType { CALL = 0, KEY_EXCHANGE = 1, ACTIVE_CALL = 2, - END_CALL = 3 + END_CALL = 3 } /** diff --git a/app/servers.ts b/app/servers.ts index aa52cbb..8a5bd15 100644 --- a/app/servers.ts +++ b/app/servers.ts @@ -1,10 +1,10 @@ export const SERVERS = [ //'wss://cdn.rosetta-im.com', //'ws://10.211.55.2:3000', - //'ws://127.0.0.1:3000', - 'wss://wss.rosetta.im' + 'ws://127.0.0.1:3000', + //'wss://wss.rosetta.im' ]; - + export function selectServer(): string { const idx = Math.floor(Math.random() * SERVERS.length); return SERVERS[idx]; From ca36a8d818128cabc4647dd569c82eb0ae1c3731 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 15:28:01 +0200 Subject: [PATCH 28/67] =?UTF-8?q?=D0=9E=D0=B1=D0=BC=D0=B5=D0=BD=20SDP,=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BD=D0=B0=D1=82=D1=8B,=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=BE=D1=80=D0=B3=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 79 +++++++++++-- .../protocol/packets/packet.signal.peer.ts | 108 ++++++++++++++++++ 2 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index b153a51..985e182 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -1,11 +1,12 @@ import { Call } from "@/app/components/Call/Call"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; -import { createContext, useEffect, useRef, useState } from "react"; +import { createContext, useRef, useState } from "react"; import nacl from 'tweetnacl'; import { useSender } from "../ProtocolProvider/useSender"; -import { PacketSignal, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal"; +import { PacketSignalPeer, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal.peer"; import { usePacket } from "../ProtocolProvider/usePacket"; import { usePublicKey } from "../AccountProvider/usePublicKey"; +import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/packets/packet.webrtc"; export interface CallContextValue { @@ -64,15 +65,40 @@ export function CallProvider(props : CallProviderProps) { const send = useSender(); const publicKey = usePublicKey(); const peerConnectionRef = useRef(null); + const roomIdRef = useRef(""); const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); - useEffect(() => { - console.info("TRACE -> ", sharedSecret) - }, [sharedSecret]); + usePacket(27, async (packet: PacketWebRTC) => { + if(!activeCall || callState != CallState.WEB_RTC_EXCHANGE){ + /** + * Нет активного звонка или мы не на стадии обмена WebRTC сигналами, игнорируем + */ + return; + } + const signalType = packet.getSignalType(); + if(signalType == WebRTCSignalType.ANSWER){ + /** + * Другая сторона (сервер SFU) отправил нам SDP ответ на наш оффер + */ + const sdp = JSON.parse(packet.getSdpOrCandidate()); + await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp)); + info("Received WebRTC answer and set remote description"); + return; + } + if(signalType == WebRTCSignalType.ICE_CANDIDATE){ + /** + * Другая сторона отправила нам ICE кандидата для установления WebRTC соединения + */ + const candidate = JSON.parse(packet.getSdpOrCandidate()); + await peerConnectionRef.current?.addIceCandidate(new RTCIceCandidate(candidate)); + info("Received WebRTC ICE candidate and added to peer connection"); + return; + } + }, [activeCall, sessionKeys, callState]); - usePacket(26, (packet: PacketSignal) => { + usePacket(26, async (packet: PacketSignalPeer) => { const signalType = packet.getSignalType(); if(activeCall){ /** @@ -123,13 +149,19 @@ export function CallProvider(props : CallProviderProps) { /** * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию */ - const signalPacket = new PacketSignal(); + const signalPacket = new PacketSignalPeer(); signalPacket.setSrc(publicKey); signalPacket.setDst(packet.getSrc()); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex')); send(signalPacket); setCallState(CallState.WEB_RTC_EXCHANGE); + /** + * Создаем комнату на сервере SFU, комнату создает звонящий + */ + let webRtcSignal = new PacketSignalPeer(); + webRtcSignal.setSignalType(SignalType.CREATE_ROOM); + send(webRtcSignal); } if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){ console.info("EXCHANGE SIGNAL RECEIVED, CALLEE ROLE"); @@ -152,6 +184,32 @@ export function CallProvider(props : CallProviderProps) { setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); setCallState(CallState.WEB_RTC_EXCHANGE); } + if(signalType == SignalType.CREATE_ROOM) { + /** + * Создана комната для обмена WebRTC потоками + */ + roomIdRef.current = packet.getRoomId(); + info("WebRTC room created with id: " + packet.getRoomId()); + /** + * Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение + */ + peerConnectionRef.current = new RTCPeerConnection({ + //Experemental + iceServers: [ + { urls: 'stun:stun.l.google.com:19302' } + ] + }); + /** + * Отправляем свой оффер другой стороне + */ + let offer = await peerConnectionRef.current.createOffer(); + peerConnectionRef.current.setLocalDescription(offer); + let offerSignal = new PacketWebRTC(); + offerSignal.setSignalType(WebRTCSignalType.OFFER); + offerSignal.setSdpOrCandidate(JSON.stringify(offer)); + send(offerSignal); + return; + } }, [activeCall, sessionKeys]); const generateSessionKeys = () => { @@ -165,7 +223,7 @@ export function CallProvider(props : CallProviderProps) { setActiveCall(dialog); setCallState(CallState.CONNECTING); setShowCallView(true); - const signalPacket = new PacketSignal(); + const signalPacket = new PacketSignalPeer(); signalPacket.setSrc(publicKey); signalPacket.setDst(dialog); signalPacket.setSignalType(SignalType.CALL); @@ -174,12 +232,13 @@ export function CallProvider(props : CallProviderProps) { } const close = () => { - const packetSignal = new PacketSignal(); + const packetSignal = new PacketSignalPeer(); packetSignal.setSrc(publicKey); packetSignal.setDst(activeCall); packetSignal.setSignalType(SignalType.END_CALL); send(packetSignal); peerConnectionRef.current = null; + roomIdRef.current = ""; setActiveCall(""); setCallState(CallState.ENDED); setShowCallView(false); @@ -199,7 +258,7 @@ export function CallProvider(props : CallProviderProps) { * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи */ const keys = generateSessionKeys(); - const signalPacket = new PacketSignal(); + const signalPacket = new PacketSignalPeer(); signalPacket.setSrc(publicKey); signalPacket.setDst(activeCall); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts new file mode 100644 index 0000000..d37d513 --- /dev/null +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts @@ -0,0 +1,108 @@ +import Packet from "../packet"; +import Stream from "../stream"; + +export enum SignalType { + CALL = 0, + KEY_EXCHANGE = 1, + ACTIVE_CALL = 2, + END_CALL = 3, + CREATE_ROOM = 4 +} + +/** + * Пакет сигналинга, для сигналов WebRTC используется отдельный пакет 27 PacketWebRTCExchange + */ +export class PacketSignalPeer extends Packet { + + private src: string = ""; + /** + * Назначение + */ + private dst: string = ""; + /** + * Используется если SignalType == KEY_EXCHANGE, для идентификации сессии обмена ключами + */ + private sharedPublic: string = ""; + + private signalType: SignalType = SignalType.CALL; + + /** + * Используется если SignalType == CREATE_ROOM, + * для идентификации комнаты на SFU сервере, в которой будет происходить обмен сигналами + * WebRTC для установления P2P соединения между участниками звонка + */ + private roomId: string = ""; + + + public getPacketId(): number { + return 26; + } + + public _receive(stream: Stream): void { + this.signalType = stream.readInt8(); + this.src = stream.readString(); + this.dst = stream.readString(); + if(this.signalType == SignalType.KEY_EXCHANGE){ + this.sharedPublic = stream.readString(); + } + if(this.signalType == SignalType.CREATE_ROOM){ + this.roomId = stream.readString(); + } + } + + public _send(): Promise | Stream { + const stream = new Stream(); + stream.writeInt16(this.getPacketId()); + stream.writeInt8(this.signalType); + stream.writeString(this.src); + stream.writeString(this.dst); + if(this.signalType == SignalType.KEY_EXCHANGE){ + stream.writeString(this.sharedPublic); + } + if(this.signalType == SignalType.CREATE_ROOM){ + stream.writeString(this.roomId); + } + return stream; + } + + public setDst(dst: string) { + this.dst = dst; + } + + public setSharedPublic(sharedPublic: string) { + this.sharedPublic = sharedPublic; + } + + public setSignalType(signalType: SignalType) { + this.signalType = signalType; + } + + public getDst(): string { + return this.dst; + } + + public getSharedPublic(): string { + return this.sharedPublic; + } + + public getSignalType(): SignalType { + return this.signalType; + } + + public getSrc(): string { + return this.src; + } + + public setSrc(src: string) { + this.src = src; + } + + public getRoomId(): string { + return this.roomId; + } + + public setRoomId(roomId: string) { + this.roomId = roomId; + } + +} \ No newline at end of file From 2707bd2a39830353ccf4095ea98d6b486e89225e Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 15:28:24 +0200 Subject: [PATCH 29/67] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20WebRTC=20SDP/ICE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../protocol/packets/packet.signal.ts | 86 ------------------- .../protocol/packets/packet.webrtc.ts | 52 +++++++++++ .../ProtocolProvider/protocol/protocol.ts | 4 +- 3 files changed, 54 insertions(+), 88 deletions(-) delete mode 100644 app/providers/ProtocolProvider/protocol/packets/packet.signal.ts create mode 100644 app/providers/ProtocolProvider/protocol/packets/packet.webrtc.ts diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts deleted file mode 100644 index 54c86b2..0000000 --- a/app/providers/ProtocolProvider/protocol/packets/packet.signal.ts +++ /dev/null @@ -1,86 +0,0 @@ -import Packet from "../packet"; -import Stream from "../stream"; - -export enum SignalType { - CALL = 0, - KEY_EXCHANGE = 1, - ACTIVE_CALL = 2, - END_CALL = 3 -} - -/** - * Пакет сигналинга, для сигналов WebRTC используется отдельный пакет 27 PacketWebRTCExchange - */ -export class PacketSignal extends Packet { - - private src: string = ""; - /** - * Назначение - */ - private dst: string = ""; - /** - * Используется если SignalType == KEY_EXCHANGE, для идентификации сессии обмена ключами - */ - private sharedPublic: string = ""; - - private signalType: SignalType = SignalType.CALL; - - - public getPacketId(): number { - return 26; - } - - public _receive(stream: Stream): void { - this.signalType = stream.readInt8(); - this.src = stream.readString(); - this.dst = stream.readString(); - if(this.signalType == SignalType.KEY_EXCHANGE){ - this.sharedPublic = stream.readString(); - } - } - - public _send(): Promise | Stream { - const stream = new Stream(); - stream.writeInt16(this.getPacketId()); - stream.writeInt8(this.signalType); - stream.writeString(this.src); - stream.writeString(this.dst); - if(this.signalType == SignalType.KEY_EXCHANGE){ - stream.writeString(this.sharedPublic); - } - return stream; - } - - public setDst(dst: string) { - this.dst = dst; - } - - public setSharedPublic(sharedPublic: string) { - this.sharedPublic = sharedPublic; - } - - public setSignalType(signalType: SignalType) { - this.signalType = signalType; - } - - public getDst(): string { - return this.dst; - } - - public getSharedPublic(): string { - return this.sharedPublic; - } - - public getSignalType(): SignalType { - return this.signalType; - } - - public getSrc(): string { - return this.src; - } - - public setSrc(src: string) { - this.src = src; - } - -} \ No newline at end of file diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.webrtc.ts b/app/providers/ProtocolProvider/protocol/packets/packet.webrtc.ts new file mode 100644 index 0000000..9a5ca74 --- /dev/null +++ b/app/providers/ProtocolProvider/protocol/packets/packet.webrtc.ts @@ -0,0 +1,52 @@ +import Packet from "../packet"; +import Stream from "../stream"; + +export enum WebRTCSignalType { + OFFER = 0, + ANSWER = 1, + ICE_CANDIDATE = 2 +} + +/** + * Пакет для обмена сигналами WebRTC, такими как оффер, ответ и ICE кандидаты. + * Используется на стадии WEB_RTC_EXCHANGE в сигналинге звонков. + */ +export class PacketWebRTC extends Packet { + + private signalType: WebRTCSignalType = WebRTCSignalType.OFFER; + private sdpOrCandidate: string = ""; + + public getPacketId(): number { + return 27; + } + + public _receive(stream: Stream): void { + this.signalType = stream.readInt8(); + this.sdpOrCandidate = stream.readString(); + } + + public _send(): Promise | Stream { + let stream = new Stream(); + stream.writeInt16(this.getPacketId()); + stream.writeInt8(this.signalType); + stream.writeString(this.sdpOrCandidate); + return stream; + } + + public setSignalType(type: WebRTCSignalType) { + this.signalType = type; + } + + public getSignalType(): WebRTCSignalType { + return this.signalType; + } + + public setSdpOrCandidate(data: string) { + this.sdpOrCandidate = data; + } + + public getSdpOrCandidate(): string { + return this.sdpOrCandidate; + } + +} \ No newline at end of file diff --git a/app/providers/ProtocolProvider/protocol/protocol.ts b/app/providers/ProtocolProvider/protocol/protocol.ts index 90d2fe0..439a03b 100644 --- a/app/providers/ProtocolProvider/protocol/protocol.ts +++ b/app/providers/ProtocolProvider/protocol/protocol.ts @@ -25,7 +25,7 @@ import { PacketDeviceNew } from "./packets/packet.device.new"; import { PacketDeviceList } from "./packets/packet.device.list"; import { PacketDeviceResolve } from "./packets/packet.device.resolve"; import { PacketSync } from "./packets/packet.sync"; -import { PacketSignal } from "./packets/packet.signal"; +import { PacketSignalPeer } from "./packets/packet.signal.peer"; export default class Protocol extends EventEmitter { private serverAddress: string; @@ -126,7 +126,7 @@ export default class Protocol extends EventEmitter { this._supportedPackets.set(0x17, new PacketDeviceList()); this._supportedPackets.set(0x18, new PacketDeviceResolve()); this._supportedPackets.set(25, new PacketSync()); - this._supportedPackets.set(26, new PacketSignal()); + this._supportedPackets.set(26, new PacketSignalPeer()); } private _findWaiters(packetId: number): ((packet: Packet) => void)[] { From 8dc2537cdcbf37c9fb12e99c78c988bc347f0127 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 15:36:26 +0200 Subject: [PATCH 30/67] =?UTF-8?q?WebRTC=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=20=D0=B2=20=D0=BF=D1=80=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/ProtocolProvider/protocol/protocol.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/providers/ProtocolProvider/protocol/protocol.ts b/app/providers/ProtocolProvider/protocol/protocol.ts index 439a03b..28c4172 100644 --- a/app/providers/ProtocolProvider/protocol/protocol.ts +++ b/app/providers/ProtocolProvider/protocol/protocol.ts @@ -26,6 +26,7 @@ import { PacketDeviceList } from "./packets/packet.device.list"; import { PacketDeviceResolve } from "./packets/packet.device.resolve"; import { PacketSync } from "./packets/packet.sync"; import { PacketSignalPeer } from "./packets/packet.signal.peer"; +import { PacketWebRTC } from "./packets/packet.webrtc"; export default class Protocol extends EventEmitter { private serverAddress: string; @@ -127,6 +128,7 @@ export default class Protocol extends EventEmitter { this._supportedPackets.set(0x18, new PacketDeviceResolve()); this._supportedPackets.set(25, new PacketSync()); this._supportedPackets.set(26, new PacketSignalPeer()); + this._supportedPackets.set(27, new PacketWebRTC()); } private _findWaiters(packetId: number): ((packet: Packet) => void)[] { From 0600da5b7caecc22bef4b5f8086749a1b912bcf2 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 15:43:49 +0200 Subject: [PATCH 31/67] =?UTF-8?q?Signal=20Peer=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20Src/Dst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 985e182..04d5e32 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -161,6 +161,8 @@ export function CallProvider(props : CallProviderProps) { */ let webRtcSignal = new PacketSignalPeer(); webRtcSignal.setSignalType(SignalType.CREATE_ROOM); + webRtcSignal.setSrc(publicKey); + webRtcSignal.setDst(packet.getSrc()); send(webRtcSignal); } if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){ From 0513a900362ce454d8e497ebec825dacb7c42c26 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 18:38:04 +0200 Subject: [PATCH 32/67] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D1=80=D0=B5=D0=BA=D0=BE=D0=B2?= =?UTF-8?q?=20(=D0=B0=D1=83=D0=B4=D0=B8=D0=BE)=20=D0=B2=20RTCPeerConnectio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 04d5e32..b6c61d2 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -104,7 +104,7 @@ export function CallProvider(props : CallProviderProps) { /** * У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка */ - if(packet.getSrc() != activeCall){ + if(packet.getSrc() != activeCall && packet.getSrc() != publicKey){ console.info("Received signal from " + packet.getSrc() + " but active call is with " + activeCall + ", ignoring"); info("Received signal for another call, ignoring"); return; @@ -201,6 +201,16 @@ export function CallProvider(props : CallProviderProps) { { urls: 'stun:stun.l.google.com:19302' } ] }); + /** + * Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести, + * когда мы установим WebRTC соединение + */ + const localStream = await navigator.mediaDevices.getUserMedia({ + audio: true + }); + localStream.getTracks().forEach(track => { + peerConnectionRef.current?.addTrack(track, localStream); + }); /** * Отправляем свой оффер другой стороне */ From 76442c4161e4cdc8900a2e5d9b681075bba675e1 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 20:23:29 +0200 Subject: [PATCH 33/67] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D1=82=D1=80=D0=B5=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B8=20IceCandidates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index b6c61d2..f5781fb 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -92,10 +92,27 @@ export function CallProvider(props : CallProviderProps) { * Другая сторона отправила нам ICE кандидата для установления WebRTC соединения */ const candidate = JSON.parse(packet.getSdpOrCandidate()); + console.info(candidate); await peerConnectionRef.current?.addIceCandidate(new RTCIceCandidate(candidate)); info("Received WebRTC ICE candidate and added to peer connection"); return; } + if(signalType == WebRTCSignalType.OFFER){ + /** + * SFU сервер отправил нам оффер, например при renegotiation, нам нужно его принять и + * отправить ответ (ANSWER) + */ + const sdp = JSON.parse(packet.getSdpOrCandidate()); + await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp)); + let answer = await peerConnectionRef.current?.createAnswer(); + await peerConnectionRef.current?.setLocalDescription(answer); + let answerSignal = new PacketWebRTC(); + answerSignal.setSignalType(WebRTCSignalType.ANSWER); + answerSignal.setSdpOrCandidate(JSON.stringify(answer)); + send(answerSignal); + info("Received WebRTC offer, set remote description and sent answer"); + return; + } }, [activeCall, sessionKeys, callState]); usePacket(26, async (packet: PacketSignalPeer) => { @@ -201,6 +218,34 @@ export function CallProvider(props : CallProviderProps) { { urls: 'stun:stun.l.google.com:19302' } ] }); + /** + * Подписываемся на ICE кандидат + */ + peerConnectionRef.current.onicecandidate = (event) => { + if(event.candidate){ + let candidateSignal = new PacketWebRTC(); + candidateSignal.setSignalType(WebRTCSignalType.ICE_CANDIDATE); + candidateSignal.setSdpOrCandidate(JSON.stringify(event.candidate)); + send(candidateSignal); + } + } + /** + * Соединение установлено, можно начинать звонок, переходим в активное состояние звонка + */ + peerConnectionRef.current.onconnectionstatechange = () => { + console.info("Peer connection state changed: " + peerConnectionRef.current?.connectionState); + if(peerConnectionRef.current?.connectionState == "connected"){ + setCallState(CallState.ACTIVE); + info("WebRTC connection established, call is active"); + } + } + + peerConnectionRef.current.ontrack = (event) => { + /** + * При получении медиа-трека с другой стороны + */ + console.info("TRACK RECV!!!!!"); + } /** * Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести, * когда мы установим WebRTC соединение From f0d09093826be58892010cbb369cd6fea8f876b0 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 23:05:54 +0200 Subject: [PATCH 34/67] =?UTF-8?q?=D0=94=D0=B8=D0=BD=D0=B0=D0=BC=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B9=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=20ICE=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 42 ++++++++++++++-- .../protocol/packets/packet.ice.servers.ts | 50 +++++++++++++++++++ .../ProtocolProvider/protocol/protocol.ts | 2 + 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index f5781fb..c62f082 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -1,12 +1,13 @@ import { Call } from "@/app/components/Call/Call"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; -import { createContext, useRef, useState } from "react"; +import { createContext, useEffect, useRef, useState } from "react"; import nacl from 'tweetnacl'; import { useSender } from "../ProtocolProvider/useSender"; import { PacketSignalPeer, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal.peer"; import { usePacket } from "../ProtocolProvider/usePacket"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/packets/packet.webrtc"; +import { PacketIceServers } from "../ProtocolProvider/protocol/packets/packet.ice.servers"; export interface CallContextValue { @@ -69,6 +70,40 @@ export function CallProvider(props : CallProviderProps) { const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); + const iceServersRef = useRef([]); + + useEffect(() => { + /** + * Нам нужно получить ICE серверы для установки соединения из разных сетей + * Получаем их от сервера + */ + let packet = new PacketIceServers(); + send(packet); + }, []); + + usePacket(28, async (packet: PacketIceServers) => { + let iceServers = packet.getIceServers(); + /** + * ICE серверы получены, теперь нужно привести их к форматку клиента и добавить udp и tcp варианты + */ + let formattedIceServers: RTCIceServer[] = []; + for(let i = 0; i < iceServers.length; i++){ + let server = iceServers[i]; + formattedIceServers.push({ + urls: "trun:" + server.urls + "?transport=udp", + username: server.username, + credential: server.credential + }); + formattedIceServers.push({ + urls: "trun:" + server.urls + "?transport=tcp", + username: server.username, + credential: server.credential + }); + } + console.info("ICE SERVERS RECEIVED: ", formattedIceServers); + iceServersRef.current = formattedIceServers; + info("Received ICE servers from server, count: " + formattedIceServers.length); + }, []); usePacket(27, async (packet: PacketWebRTC) => { if(!activeCall || callState != CallState.WEB_RTC_EXCHANGE){ @@ -213,10 +248,7 @@ export function CallProvider(props : CallProviderProps) { * Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение */ peerConnectionRef.current = new RTCPeerConnection({ - //Experemental - iceServers: [ - { urls: 'stun:stun.l.google.com:19302' } - ] + iceServers: iceServersRef.current }); /** * Подписываемся на ICE кандидат diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts b/app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts new file mode 100644 index 0000000..69a598b --- /dev/null +++ b/app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts @@ -0,0 +1,50 @@ +import Packet from "../packet"; +import Stream from "../stream"; + +export class PacketIceServers extends Packet { + private iceServers: RTCIceServer[] = []; + + public getPacketId(): number { + return 28; + } + + public _receive(stream: Stream): void { + const serversCount = stream.readInt16(); + this.iceServers = []; + for(let i = 0; i < serversCount; i++){ + const urls = stream.readString(); + const username = stream.readString(); + const credential = stream.readString(); + this.iceServers.push({ + urls, + username, + credential + }); + } + } + + public _send(): Promise | Stream { + const stream = new Stream(); + stream.writeInt16(this.getPacketId()); + stream.writeInt16(this.iceServers.length); + for(let i = 0; i < this.iceServers.length; i++){ + const server = this.iceServers[i]; + /** + * Не поддерживает массив urls!!! + */ + stream.writeString((server.urls as string)); + stream.writeString(server.username || ""); + stream.writeString(server.credential || ""); + } + return stream; + } + + public getIceServers(): RTCIceServer[] { + return this.iceServers; + } + + public setIceServers(servers: RTCIceServer[]) { + this.iceServers = servers; + } + +} \ No newline at end of file diff --git a/app/providers/ProtocolProvider/protocol/protocol.ts b/app/providers/ProtocolProvider/protocol/protocol.ts index 28c4172..c9e6822 100644 --- a/app/providers/ProtocolProvider/protocol/protocol.ts +++ b/app/providers/ProtocolProvider/protocol/protocol.ts @@ -27,6 +27,7 @@ import { PacketDeviceResolve } from "./packets/packet.device.resolve"; import { PacketSync } from "./packets/packet.sync"; import { PacketSignalPeer } from "./packets/packet.signal.peer"; import { PacketWebRTC } from "./packets/packet.webrtc"; +import { PacketIceServers } from "./packets/packet.ice.servers"; export default class Protocol extends EventEmitter { private serverAddress: string; @@ -129,6 +130,7 @@ export default class Protocol extends EventEmitter { this._supportedPackets.set(25, new PacketSync()); this._supportedPackets.set(26, new PacketSignalPeer()); this._supportedPackets.set(27, new PacketWebRTC()); + this._supportedPackets.set(28, new PacketIceServers()); } private _findWaiters(packetId: number): ((packet: Packet) => void)[] { From f57ec484e3d651159374126e7a728842914dfae9 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 14 Mar 2026 23:13:00 +0200 Subject: [PATCH 35/67] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=BF=D0=B5=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20TURN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 5 ++--- app/servers.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index c62f082..8f2dd7f 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -90,17 +90,16 @@ export function CallProvider(props : CallProviderProps) { for(let i = 0; i < iceServers.length; i++){ let server = iceServers[i]; formattedIceServers.push({ - urls: "trun:" + server.urls + "?transport=udp", + urls: "turn:" + server.urls + "?transport=udp", username: server.username, credential: server.credential }); formattedIceServers.push({ - urls: "trun:" + server.urls + "?transport=tcp", + urls: "turn:" + server.urls + "?transport=tcp", username: server.username, credential: server.credential }); } - console.info("ICE SERVERS RECEIVED: ", formattedIceServers); iceServersRef.current = formattedIceServers; info("Received ICE servers from server, count: " + formattedIceServers.length); }, []); diff --git a/app/servers.ts b/app/servers.ts index 8a5bd15..be66ffa 100644 --- a/app/servers.ts +++ b/app/servers.ts @@ -1,7 +1,7 @@ export const SERVERS = [ //'wss://cdn.rosetta-im.com', //'ws://10.211.55.2:3000', - 'ws://127.0.0.1:3000', + 'ws://192.168.6.82:3000', //'wss://wss.rosetta.im' ]; From ab57303eb6c2e8275eb063e1527959fc91cfcb95 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sun, 15 Mar 2026 17:22:48 +0200 Subject: [PATCH 36/67] =?UTF-8?q?=D0=91=D1=83=D1=84=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20ICE=20=D0=BA=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D0=B8=D0=B4=D0=B0=D1=82=D0=BE=D0=B2=20(=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=B1=D0=B5=D0=B6=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B3?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 67 ++++++++++++++++++--- app/servers.ts | 4 +- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 8f2dd7f..0e30063 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -71,15 +71,40 @@ export function CallProvider(props : CallProviderProps) { const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); const iceServersRef = useRef([]); + const remoteAudioRef = useRef(null); + const iceCandidatesBufferRef = useRef([]); useEffect(() => { - /** + /** * Нам нужно получить ICE серверы для установки соединения из разных сетей * Получаем их от сервера */ let packet = new PacketIceServers(); send(packet); - }, []); + + //debug + + setInterval(async () => { + if(callState == CallState.ACTIVE){ + if(peerConnectionRef.current){ + const stats = await peerConnectionRef.current.getStats(); + stats.forEach((report) => { + + if (report.type === "inbound-rtp" && !report.isRemote) { + const kind = (report as any).kind || (report as any).mediaType; + const bytesReceived = (report as any).bytesReceived ?? 0; + const packetsReceived = (report as any).packetsReceived ?? 0; + const packetsLost = (report as any).packetsLost ?? 0; + + console.log( + `[inbound ${kind}] bytesReceived=${bytesReceived}, packetsReceived=${packetsReceived}, packetsLost=${packetsLost}` + ); + } + }); + } + } + }, 2000); + }, [callState, peerConnectionRef]); usePacket(28, async (packet: PacketIceServers) => { let iceServers = packet.getIceServers(); @@ -118,6 +143,15 @@ export function CallProvider(props : CallProviderProps) { */ const sdp = JSON.parse(packet.getSdpOrCandidate()); await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp)); + if(iceCandidatesBufferRef.current.length > 0){ + /** + * У нас есть буферизированные ICE кандидаты, которые мы получили до установки удаленного описания, теперь мы можем их добавить в PeerConnection + */ + for(let i = 0; i < iceCandidatesBufferRef.current.length; i++){ + await peerConnectionRef.current?.addIceCandidate(iceCandidatesBufferRef.current[i]); + } + iceCandidatesBufferRef.current = []; + } info("Received WebRTC answer and set remote description"); return; } @@ -127,6 +161,14 @@ export function CallProvider(props : CallProviderProps) { */ const candidate = JSON.parse(packet.getSdpOrCandidate()); console.info(candidate); + if(peerConnectionRef.current?.remoteDescription == null){ + /** + * Удаленное описание еще не установлено, буферизуем кандидата, чтобы добавить его после установки удаленного описания + */ + iceCandidatesBufferRef.current.push(new RTCIceCandidate(candidate)); + info("Received WebRTC ICE candidate but remote description is not set yet, buffering candidate"); + return; + } await peerConnectionRef.current?.addIceCandidate(new RTCIceCandidate(candidate)); info("Received WebRTC ICE candidate and added to peer connection"); return; @@ -165,13 +207,7 @@ export function CallProvider(props : CallProviderProps) { /** * Сбросили звонок */ - setActiveCall(""); - setCallState(CallState.ENDED); - setShowCallView(false); - setSessionKeys(null); - setSharedSecret(""); - setDuration(0); - roleRef.current = null; + end(); return; } if(signalType == SignalType.CALL){ @@ -276,7 +312,12 @@ export function CallProvider(props : CallProviderProps) { * При получении медиа-трека с другой стороны */ console.info("TRACK RECV!!!!!"); + if(remoteAudioRef.current){ + console.info(event.streams); + remoteAudioRef.current.srcObject = event.streams[0]; + } } + /** * Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести, * когда мы установим WebRTC соединение @@ -291,7 +332,7 @@ export function CallProvider(props : CallProviderProps) { * Отправляем свой оффер другой стороне */ let offer = await peerConnectionRef.current.createOffer(); - peerConnectionRef.current.setLocalDescription(offer); + await peerConnectionRef.current.setLocalDescription(offer); let offerSignal = new PacketWebRTC(); offerSignal.setSignalType(WebRTCSignalType.OFFER); offerSignal.setSdpOrCandidate(JSON.stringify(offer)); @@ -325,6 +366,11 @@ export function CallProvider(props : CallProviderProps) { packetSignal.setDst(activeCall); packetSignal.setSignalType(SignalType.END_CALL); send(packetSignal); + end(); + } + + const end = () => { + peerConnectionRef.current?.close(); peerConnectionRef.current = null; roomIdRef.current = ""; setActiveCall(""); @@ -386,6 +432,7 @@ export function CallProvider(props : CallProviderProps) { return ( {props.children} + ) diff --git a/app/servers.ts b/app/servers.ts index be66ffa..ce396d3 100644 --- a/app/servers.ts +++ b/app/servers.ts @@ -1,7 +1,7 @@ export const SERVERS = [ //'wss://cdn.rosetta-im.com', - //'ws://10.211.55.2:3000', - 'ws://192.168.6.82:3000', + 'ws://10.211.55.2:3000', + //'ws://192.168.6.82:3000', //'wss://wss.rosetta.im' ]; From 2c026d596dffdb405f9dcca90ce3838884e26125 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Mon, 16 Mar 2026 19:27:16 +0200 Subject: [PATCH 37/67] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20SDPOffer=20=D0=BF=D1=80=D0=B8=20renegotiat?= =?UTF-8?q?ion=20=D0=BE=D1=82=20SFU?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 49 ++++++--------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 0e30063..d948a4a 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -81,30 +81,7 @@ export function CallProvider(props : CallProviderProps) { */ let packet = new PacketIceServers(); send(packet); - - //debug - - setInterval(async () => { - if(callState == CallState.ACTIVE){ - if(peerConnectionRef.current){ - const stats = await peerConnectionRef.current.getStats(); - stats.forEach((report) => { - - if (report.type === "inbound-rtp" && !report.isRemote) { - const kind = (report as any).kind || (report as any).mediaType; - const bytesReceived = (report as any).bytesReceived ?? 0; - const packetsReceived = (report as any).packetsReceived ?? 0; - const packetsLost = (report as any).packetsLost ?? 0; - - console.log( - `[inbound ${kind}] bytesReceived=${bytesReceived}, packetsReceived=${packetsReceived}, packetsLost=${packetsLost}` - ); - } - }); - } - } - }, 2000); - }, [callState, peerConnectionRef]); + }, []); usePacket(28, async (packet: PacketIceServers) => { let iceServers = packet.getIceServers(); @@ -130,7 +107,7 @@ export function CallProvider(props : CallProviderProps) { }, []); usePacket(27, async (packet: PacketWebRTC) => { - if(!activeCall || callState != CallState.WEB_RTC_EXCHANGE){ + if(callState != CallState.WEB_RTC_EXCHANGE && callState != CallState.ACTIVE){ /** * Нет активного звонка или мы не на стадии обмена WebRTC сигналами, игнорируем */ @@ -178,18 +155,18 @@ export function CallProvider(props : CallProviderProps) { * SFU сервер отправил нам оффер, например при renegotiation, нам нужно его принять и * отправить ответ (ANSWER) */ - const sdp = JSON.parse(packet.getSdpOrCandidate()); - await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp)); - let answer = await peerConnectionRef.current?.createAnswer(); - await peerConnectionRef.current?.setLocalDescription(answer); - let answerSignal = new PacketWebRTC(); - answerSignal.setSignalType(WebRTCSignalType.ANSWER); - answerSignal.setSdpOrCandidate(JSON.stringify(answer)); - send(answerSignal); - info("Received WebRTC offer, set remote description and sent answer"); - return; + const sdp = JSON.parse(packet.getSdpOrCandidate()); + await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp)); + let answer = await peerConnectionRef.current?.createAnswer(); + await peerConnectionRef.current?.setLocalDescription(answer); + let answerSignal = new PacketWebRTC(); + answerSignal.setSignalType(WebRTCSignalType.ANSWER); + answerSignal.setSdpOrCandidate(JSON.stringify(answer)); + send(answerSignal); + info("Received WebRTC offer, set remote description and sent answer"); + return; } - }, [activeCall, sessionKeys, callState]); + }, [activeCall, sessionKeys, callState, roomIdRef]); usePacket(26, async (packet: PacketSignalPeer) => { const signalType = packet.getSignalType(); From 6dd348230fe466827aebd0d71f6e2ea16714a031 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 17 Mar 2026 15:02:57 +0200 Subject: [PATCH 38/67] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=B8=D0=BD=D0=B0=D0=BC=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D0=B0=20=D1=82=D1=80=D0=B0=D0=BD=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=BD=D1=8B=D1=85=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B2=20=D1=81=D0=BE=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B8=20=D1=81=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=B2=20g365sfu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 7 +---- .../protocol/packets/packet.ice.servers.ts | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index d948a4a..9aeb389 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -92,12 +92,7 @@ export function CallProvider(props : CallProviderProps) { for(let i = 0; i < iceServers.length; i++){ let server = iceServers[i]; formattedIceServers.push({ - urls: "turn:" + server.urls + "?transport=udp", - username: server.username, - credential: server.credential - }); - formattedIceServers.push({ - urls: "turn:" + server.urls + "?transport=tcp", + urls: "turn:" + server.url + "?transport=" + server.transport, username: server.username, credential: server.credential }); diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts b/app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts index 69a598b..2a8bacd 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.ice.servers.ts @@ -1,8 +1,15 @@ import Packet from "../packet"; import Stream from "../stream"; +export interface G365IceServer { + url: string; + username: string; + credential: string; + transport: string; +} + export class PacketIceServers extends Packet { - private iceServers: RTCIceServer[] = []; + private iceServers: G365IceServer[] = []; public getPacketId(): number { return 28; @@ -12,13 +19,15 @@ export class PacketIceServers extends Packet { const serversCount = stream.readInt16(); this.iceServers = []; for(let i = 0; i < serversCount; i++){ - const urls = stream.readString(); + const url = stream.readString(); const username = stream.readString(); const credential = stream.readString(); + const transport = stream.readString(); this.iceServers.push({ - urls, + url, username, - credential + credential, + transport }); } } @@ -29,21 +38,19 @@ export class PacketIceServers extends Packet { stream.writeInt16(this.iceServers.length); for(let i = 0; i < this.iceServers.length; i++){ const server = this.iceServers[i]; - /** - * Не поддерживает массив urls!!! - */ - stream.writeString((server.urls as string)); + stream.writeString(server.url); stream.writeString(server.username || ""); stream.writeString(server.credential || ""); + stream.writeString(server.transport || ""); } return stream; } - public getIceServers(): RTCIceServer[] { + public getIceServers(): G365IceServer[] { return this.iceServers; } - public setIceServers(servers: RTCIceServer[]) { + public setIceServers(servers: G365IceServer[]) { this.iceServers = servers; } From fcf4204063ac2370eb38436a5ad69e512c0d5a44 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 17 Mar 2026 18:39:06 +0200 Subject: [PATCH 39/67] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20END=5FCALL=5FBECAUSE=5FBUSY=20=D0=B8=20END?= =?UTF-8?q?=5FCALL=5FBECAUSE=5FPEER=5FDISCONNECTED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 42 +++++++++++++++++++ .../protocol/packets/packet.signal.peer.ts | 10 ++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 9aeb389..72ba76c 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -8,6 +8,8 @@ import { usePacket } from "../ProtocolProvider/usePacket"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/packets/packet.webrtc"; import { PacketIceServers } from "../ProtocolProvider/protocol/packets/packet.ice.servers"; +import { modals } from "@mantine/modals"; +import { Button, Flex, Text } from "@mantine/core"; export interface CallContextValue { @@ -165,6 +167,46 @@ export function CallProvider(props : CallProviderProps) { usePacket(26, async (packet: PacketSignalPeer) => { const signalType = packet.getSignalType(); + if(signalType == SignalType.END_CALL_BECAUSE_BUSY) { + modals.open({ + title: 'Busy', + centered: true, + children: ( + <> + + Line is busy, the user is currently on another call. Please try again later. + + + + + + ), + withCloseButton: false + }); + end(); + } + if(signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) { + modals.open({ + title: 'Connection lost', + centered: true, + children: ( + <> + + The connection with the user was lost. The call has ended. + + + + + + ), + withCloseButton: false + }); + end(); + } if(activeCall){ /** * У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts index d37d513..1ad3d38 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts @@ -6,7 +6,9 @@ export enum SignalType { KEY_EXCHANGE = 1, ACTIVE_CALL = 2, END_CALL = 3, - CREATE_ROOM = 4 + CREATE_ROOM = 4, + END_CALL_BECAUSE_PEER_DISCONNECTED = 5, + END_CALL_BECAUSE_BUSY = 6 } /** @@ -40,6 +42,9 @@ export class PacketSignalPeer extends Packet { public _receive(stream: Stream): void { this.signalType = stream.readInt8(); + if(this.signalType == SignalType.END_CALL_BECAUSE_BUSY || this.signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED){ + return; + } this.src = stream.readString(); this.dst = stream.readString(); if(this.signalType == SignalType.KEY_EXCHANGE){ @@ -54,6 +59,9 @@ export class PacketSignalPeer extends Packet { const stream = new Stream(); stream.writeInt16(this.getPacketId()); stream.writeInt8(this.signalType); + if(this.signalType == SignalType.END_CALL_BECAUSE_BUSY || this.signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED){ + return stream; + } stream.writeString(this.src); stream.writeString(this.dst); if(this.signalType == SignalType.KEY_EXCHANGE){ From 7b9936dcc45f0e8e2141409f88c38b14b6dc7d56 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Tue, 17 Mar 2026 19:19:36 +0200 Subject: [PATCH 40/67] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=BE=D1=82=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D1=81=D0=B5=D1=82=D0=B5=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20renegotation=20=D0=B7=D0=B0=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=88=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 72ba76c..1c7191f 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -147,7 +147,7 @@ export function CallProvider(props : CallProviderProps) { info("Received WebRTC ICE candidate and added to peer connection"); return; } - if(signalType == WebRTCSignalType.OFFER){ + if(signalType == WebRTCSignalType.OFFER && peerConnectionRef.current){ /** * SFU сервер отправил нам оффер, например при renegotiation, нам нужно его принять и * отправить ответ (ANSWER) From 88288317ab94a8a29d6c8184a847ee09beb932e5 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 18 Mar 2026 18:27:39 +0200 Subject: [PATCH 41/67] =?UTF-8?q?=D0=A1=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D0=B9=D0=BD=D1=8B=D0=B5=20=D0=B7=D0=B2=D1=83=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=B0=20(=D1=81=D0=B1=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=B8=D1=82=D1=8C,=20=D0=BC=D1=83=D1=82=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3,=20=D0=B8=20=D0=BF=D1=80=D0=BE=D1=87=D0=B5=D0=B5..?= =?UTF-8?q?.)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 168 +++++++++++++++----- lib/preload/preload.ts | 25 +++ resources/sounds/calling.mp3 | Bin 0 -> 100465 bytes resources/sounds/connected.mp3 | Bin 0 -> 47179 bytes resources/sounds/end_call.mp3 | Bin 0 -> 41951 bytes resources/sounds/micro_disable.mp3 | Bin 0 -> 22102 bytes resources/sounds/micro_enable.mp3 | Bin 0 -> 22102 bytes resources/sounds/ringtone.mp3 | Bin 0 -> 130785 bytes resources/sounds/sound_disable.mp3 | Bin 0 -> 32551 bytes resources/sounds/sound_enable.mp3 | Bin 0 -> 35685 bytes 10 files changed, 153 insertions(+), 40 deletions(-) create mode 100644 resources/sounds/calling.mp3 create mode 100644 resources/sounds/connected.mp3 create mode 100644 resources/sounds/end_call.mp3 create mode 100644 resources/sounds/micro_disable.mp3 create mode 100644 resources/sounds/micro_enable.mp3 create mode 100644 resources/sounds/ringtone.mp3 create mode 100644 resources/sounds/sound_disable.mp3 create mode 100644 resources/sounds/sound_enable.mp3 diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 1c7191f..36202f1 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -10,7 +10,8 @@ import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/pac import { PacketIceServers } from "../ProtocolProvider/protocol/packets/packet.ice.servers"; import { modals } from "@mantine/modals"; import { Button, Flex, Text } from "@mantine/core"; - +import { useSound } from "@/app/hooks/useSound"; +import useWindow from "@/app/hooks/useWindow"; export interface CallContextValue { call: (callable: string) => void; @@ -59,8 +60,9 @@ export interface CallProviderProps { export function CallProvider(props : CallProviderProps) { const [activeCall, setActiveCall] = useState(""); const [callState, setCallState] = useState(CallState.ENDED); - const [muted, setMuted] = useState(false); - const [sound, setSound] = useState(true); + const [muted, setMutedState] = useState(false); + const [sound, setSoundState] = useState(true); + const durationIntervalRef = useRef(null); const [duration, setDuration] = useState(0); const [showCallView, setShowCallView] = useState(callState == CallState.INCOMING); const {info} = useConsoleLogger("CallProvider"); @@ -73,8 +75,26 @@ export function CallProvider(props : CallProviderProps) { const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); const iceServersRef = useRef([]); - const remoteAudioRef = useRef(null); + const remoteAudioRef = useRef(null); const iceCandidatesBufferRef = useRef([]); + const mutedRef = useRef(false); + const soundRef = useRef(true); + + const {playSound, stopSound, stopLoopSound} = useSound(); + const {setWindowPriority} = useWindow(); + + + useEffect(() => { + if(callState == CallState.ACTIVE){ + stopLoopSound(); + stopSound(); + playSound("connected.mp3"); + setWindowPriority(false); + durationIntervalRef.current = setInterval(() => { + setDuration(prev => prev + 1); + }, 1000); + } + }, [callState]); useEffect(() => { /** @@ -83,6 +103,16 @@ export function CallProvider(props : CallProviderProps) { */ let packet = new PacketIceServers(); send(packet); + + return () => { + stopSound(); + if (remoteAudioRef.current) { + remoteAudioRef.current.pause(); + remoteAudioRef.current.srcObject = null; + } + peerConnectionRef.current?.close(); + peerConnectionRef.current = null; + }; }, []); usePacket(28, async (packet: PacketIceServers) => { @@ -168,43 +198,11 @@ export function CallProvider(props : CallProviderProps) { usePacket(26, async (packet: PacketSignalPeer) => { const signalType = packet.getSignalType(); if(signalType == SignalType.END_CALL_BECAUSE_BUSY) { - modals.open({ - title: 'Busy', - centered: true, - children: ( - <> - - Line is busy, the user is currently on another call. Please try again later. - - - - - - ), - withCloseButton: false - }); + openCallsModal("Line is busy, the user is currently on another call. Please try again later."); end(); } if(signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) { - modals.open({ - title: 'Connection lost', - centered: true, - children: ( - <> - - The connection with the user was lost. The call has ended. - - - - - - ), - withCloseButton: false - }); + openCallsModal("The connection with the user was lost. The call has ended.") end(); } if(activeCall){ @@ -228,6 +226,8 @@ export function CallProvider(props : CallProviderProps) { /** * Нам поступает звонок */ + setWindowPriority(true); + playSound("ringtone.mp3", true); setActiveCall(packet.getSrc()); setCallState(CallState.INCOMING); setShowCallView(true); @@ -325,10 +325,13 @@ export function CallProvider(props : CallProviderProps) { /** * При получении медиа-трека с другой стороны */ - console.info("TRACK RECV!!!!!"); - if(remoteAudioRef.current){ + if(remoteAudioRef.current && event.streams[0]){ console.info(event.streams); remoteAudioRef.current.srcObject = event.streams[0]; + remoteAudioRef.current.muted = !soundRef.current; + void remoteAudioRef.current.play().catch((e) => { + console.error("Failed to play remote audio:", e); + }); } } @@ -355,6 +358,25 @@ export function CallProvider(props : CallProviderProps) { } }, [activeCall, sessionKeys]); + const openCallsModal = (text : string) => { + modals.open({ + centered: true, + children: ( + <> + + {text} + + + + + + ), + withCloseButton: false + }); + } + const generateSessionKeys = () => { const sessionKeys = nacl.box.keyPair(); info("Generated keys for call session, len: " + sessionKeys.publicKey.length); @@ -363,6 +385,14 @@ export function CallProvider(props : CallProviderProps) { } const call = (dialog: string) => { + if(callState == CallState.ACTIVE + || callState == CallState.CONNECTING + || callState == CallState.KEY_EXCHANGE + || callState == CallState.WEB_RTC_EXCHANGE){ + openCallsModal("You are already on a call, please end the current call before starting a new one."); + return; + } + setWindowPriority(false); setActiveCall(dialog); setCallState(CallState.CONNECTING); setShowCallView(true); @@ -372,6 +402,7 @@ export function CallProvider(props : CallProviderProps) { signalPacket.setSignalType(SignalType.CALL); send(signalPacket); roleRef.current = CallRole.CALLER; + playSound("calling.mp3", true); } const close = () => { @@ -384,14 +415,28 @@ export function CallProvider(props : CallProviderProps) { } const end = () => { + stopLoopSound(); + stopSound(); + if (remoteAudioRef.current) { + remoteAudioRef.current.pause(); + remoteAudioRef.current.srcObject = null; + } + setDuration(0); + durationIntervalRef.current && clearInterval(durationIntervalRef.current); + setWindowPriority(false); + playSound("end_call.mp3"); peerConnectionRef.current?.close(); peerConnectionRef.current = null; roomIdRef.current = ""; + mutedRef.current = false; + soundRef.current = true; setActiveCall(""); setCallState(CallState.ENDED); setShowCallView(false); setSessionKeys(null); setDuration(0); + setMutedState(false); + setSoundState(true); roleRef.current = null; } @@ -402,6 +447,9 @@ export function CallProvider(props : CallProviderProps) { */ return; } + setWindowPriority(false); + stopLoopSound(); + stopSound(); /** * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи */ @@ -428,6 +476,46 @@ export function CallProvider(props : CallProviderProps) { return sharedSecret; } + + const setMuted = (nextMuted: boolean) => { + if (mutedRef.current === nextMuted) { + return; + } + + mutedRef.current = nextMuted; + playSound(nextMuted ? "micro_enable.mp3" : "micro_disable.mp3"); + + if(peerConnectionRef.current){ + peerConnectionRef.current.getSenders().forEach(sender => { + if(sender.track?.kind == "audio"){ + sender.track.enabled = !nextMuted; + } + }); + } + + setMutedState(nextMuted); + } + + const setSound = (nextSound: boolean) => { + if (soundRef.current === nextSound) { + return; + } + + soundRef.current = nextSound; + playSound(nextSound ? "sound_enable.mp3" : "sound_disable.mp3"); + + if(remoteAudioRef.current){ + remoteAudioRef.current.muted = !nextSound; + if (nextSound) { + void remoteAudioRef.current.play().catch((e) => { + console.error("Failed to resume remote audio:", e); + }); + } + } + + setSoundState(nextSound); + } + const context = { call, close, diff --git a/lib/preload/preload.ts b/lib/preload/preload.ts index ce67cae..dbe848a 100644 --- a/lib/preload/preload.ts +++ b/lib/preload/preload.ts @@ -1,8 +1,23 @@ import { contextBridge, ipcRenderer, shell } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import api from './api' +import { pathToFileURL } from 'node:url' +import path from 'node:path' +import fs from "node:fs"; +function resolveSound(fileName: string) { + const isDev = !process.env.APP_PACKAGED; // или свой флаг dev + const fullPath = isDev + ? path.join(process.cwd(), "resources", "sounds", fileName) + : path.join(process.resourcesPath, "resources", "sounds", fileName); + + if (!fs.existsSync(fullPath)) { + throw new Error(`Sound not found: ${fullPath}`); + } + return pathToFileURL(fullPath).toString(); +} + const exposeContext = async () => { if (process.contextIsolated) { try { @@ -16,6 +31,11 @@ const exposeContext = async () => { ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath); } }); + contextBridge.exposeInMainWorld("mediaApi", { + getSoundUrl: (fileName: string) => { + return resolveSound(fileName); + } + }); } catch (error) { console.error(error) } @@ -23,6 +43,11 @@ const exposeContext = async () => { window.electron = electronAPI window.api = api; window.shell = shell; + window.mediaApi = { + getSoundUrl: (fileName: string) => { + return resolveSound(fileName); + } + } } } diff --git a/resources/sounds/calling.mp3 b/resources/sounds/calling.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5a00cd0c477aa5ed8b3ee07aab8c643c5575a519 GIT binary patch literal 100465 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
  • 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
    IPt!f?q#sa)! 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

    ujdvZK|_*CR9zk$nGu`YMgR+G}}gmx~6#3 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 literal 0 HcmV?d00001 diff --git a/resources/sounds/connected.mp3 b/resources/sounds/connected.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3e030facef38f7be43415f83db8daf7eca846dbe GIT binary patch literal 47179 zcmeFYWmH?yw)dUjf#4S0-GUW|;I73r1PfZAK%q#3yF10DIK>MT+Tc!c3WXLg+5!a% zZ7C1uJm-Dh=ffR$oN?bf-cNUqvByrb)*jiJS@XB&{I9h%RYWlX|1c_JRXyd0Gx*_9 z^t5+$@_))~;QQqM_om}Nn*TG?^m2ak&`$ht@B;vE>H*l;1O%j{)YQ}r46Ll&+=7CF zVq((LFqo>UsexIIBA03deH zj+d(o0Ky*bgO31-ne>l_|FQq^!2fvQzs~~?Oi%#;a0p=i*-ro%qbG098f5{F8FYNz zzX4d!2lgZsU_gv4j7z-!DQgd~DI7bdX4iX8?I6(p=&NU-KQV?bC!)Zu_LUnajNTy< z1I?w!kkqejY9aO_Q6z?Wa^tstMLLoB%NMJI=@f4X-+#Zmv47Ta(>FGR$jmyA^|c5v zqkB`QvF>H}#Dqsk>&uw$Q@_jGqm1I>&oAz8=Z%ca_x)}clP6D)@wFjOz|cYB`{kg& z&6SlcJvq>Wz(CylO}6{N!ND�Gj?A0N(wStn5FW1C~`lV}AJy0nVP^-xr`u4|N=p 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%0Dem1~$%v+rXj(7bxB@~zg#%$$$ zxTW)$7~5pGy=O0PMN8OR(;tO6iL%L^nuAZ8A}s4*iDkA-@bqvc z87yHhmHLx1vG7Uq;KA1OKBs7H;bB&)=sCgQ>e=>V`;%HzirTgJHiJQ`Augx9F&qb4 z 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 literal 0 HcmV?d00001 diff --git a/resources/sounds/micro_disable.mp3 b/resources/sounds/micro_disable.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..b87de43ce0a17e04c353b93c2ce7a9ee77b90031 GIT binary patch literal 22102 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

    (1rl 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 C)RfBr literal 0 HcmV?d00001 diff --git a/resources/sounds/sound_enable.mp3 b/resources/sounds/sound_enable.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..797c56ec28f7c5cc37aa956caebdb65ad2132acf GIT binary patch literal 35685 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 c{ER3#SJ3RjYxpCp7#e)W+GPBs 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 literal 0 HcmV?d00001 From 41d7a898306e804fd8e411ebf73d7a0a8e473580 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 18 Mar 2026 18:28:10 +0200 Subject: [PATCH 42/67] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=D0=BD=D1=8B=D0=BC=20=D0=B7?= =?UTF-8?q?=D0=B2=D1=83=D0=BA=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/hooks/useSound.ts | 78 ++++++++++++++++++++++++++++++++++++++++++ lib/preload/index.d.ts | 3 ++ 2 files changed, 81 insertions(+) create mode 100644 app/hooks/useSound.ts diff --git a/app/hooks/useSound.ts b/app/hooks/useSound.ts new file mode 100644 index 0000000..44ffac8 --- /dev/null +++ b/app/hooks/useSound.ts @@ -0,0 +1,78 @@ +import { useRef } from "react"; + +export function useSound() { + const audioRef = useRef(null); + const loopingAudioRef = useRef(null); + + const stopSound = () => { + if (!audioRef.current) { + return; + } + audioRef.current.pause(); + audioRef.current.currentTime = 0; + audioRef.current.removeAttribute("src"); + audioRef.current.load(); + }; + + const playSound = (sound : string, loop: boolean = false) => { + try { + if(loop){ + if (!loopingAudioRef.current) { + loopingAudioRef.current = new Audio(); + loopingAudioRef.current.volume = 0.1; + loopingAudioRef.current.preload = "auto"; + loopingAudioRef.current.loop = true; + } + + const url = window.mediaApi.getSoundUrl(sound); + const player = loopingAudioRef.current; + + player.src = url; + const playPromise = player.play(); + if (playPromise) { + void playPromise.catch((e) => { + console.error("Failed to play looping UI sound:", e); + }); + } + return; + } + if (!audioRef.current) { + audioRef.current = new Audio(); + audioRef.current.volume = 0.1; + audioRef.current.preload = "auto"; + audioRef.current.loop = loop; + } + + const url = window.mediaApi.getSoundUrl(sound); + const player = audioRef.current; + + stopSound(); + + player.src = url; + const playPromise = player.play(); + if (playPromise) { + void playPromise.catch((e) => { + console.error("Failed to play UI sound:", e); + }); + } + } catch (e) { + console.error("Failed to prepare UI sound:", e); + } + } + + const stopLoopSound = () => { + if (!loopingAudioRef.current) { + return; + } + loopingAudioRef.current.pause(); + loopingAudioRef.current.currentTime = 0; + loopingAudioRef.current.removeAttribute("src"); + loopingAudioRef.current.load(); + } + + return { + playSound, + stopSound, + stopLoopSound + } +} \ No newline at end of file diff --git a/lib/preload/index.d.ts b/lib/preload/index.d.ts index 35fa486..284361b 100644 --- a/lib/preload/index.d.ts +++ b/lib/preload/index.d.ts @@ -13,5 +13,8 @@ declare global { downloadsPath: string; deviceName: string; deviceId: string; + mediaApi: { + getSoundUrl: (fileName: string) => string; + }; } } From 824b1fec65800b9ad5c1515694a9a6f101d50dee Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 18 Mar 2026 18:28:37 +0200 Subject: [PATCH 43/67] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B2=20=D1=8F=D0=B4=D1=80=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=D0=B7=D0=B0=20=D0=BE=D0=BA=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=B5=D1=80=D1=85=20=D0=B2=D1=81=D0=B5=D1=85?= =?UTF-8?q?=20=D0=BE=D0=BA=D0=BE=D0=BD=20=D0=BF=D1=80=D0=B8=20=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ActiveCall/ActiveCall.tsx | 5 +++- app/components/Call/Call.tsx | 8 ++--- app/hooks/useWindow.ts | 11 ++++++- lib/main/app.ts | 38 ++++++++++++++++++++++-- package.json | 5 +++- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/app/components/ActiveCall/ActiveCall.tsx b/app/components/ActiveCall/ActiveCall.tsx index ed7f027..b3cbdb3 100644 --- a/app/components/ActiveCall/ActiveCall.tsx +++ b/app/components/ActiveCall/ActiveCall.tsx @@ -16,7 +16,10 @@ export function ActiveCall() { } const getConnectingClass = () => { - if(callState === CallState.CONNECTING){ + if(callState === CallState.CONNECTING + || callState === CallState.INCOMING + || callState === CallState.KEY_EXCHANGE + || callState === CallState.WEB_RTC_EXCHANGE){ return classes.connecting; } if(callState === CallState.ACTIVE){ diff --git a/app/components/Call/Call.tsx b/app/components/Call/Call.tsx index 0b8b8b1..76a8908 100644 --- a/app/components/Call/Call.tsx +++ b/app/components/Call/Call.tsx @@ -40,13 +40,13 @@ export function Call(props: CallProps) { setShowCallView(false)} justify={'center'} align={'center'}> - - Back + + Back - + - + diff --git a/app/hooks/useWindow.ts b/app/hooks/useWindow.ts index ea5d28f..cd3ff31 100644 --- a/app/hooks/useWindow.ts +++ b/app/hooks/useWindow.ts @@ -20,10 +20,19 @@ const useWindow = () => { window.api.send('window-theme', theme); } + const setWindowPriority = (isTop: boolean) => { + if(isTop){ + window.api.invoke('window-top'); + } else { + window.api.invoke('window-priority-normal'); + } + } + return { setSize, setResizeble, - setTheme + setTheme, + setWindowPriority } } diff --git a/lib/main/app.ts b/lib/main/app.ts index 04ea6eb..38461f7 100644 --- a/lib/main/app.ts +++ b/lib/main/app.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, shell, ipcMain, nativeTheme, screen, powerMonitor } from 'electron' +import { BrowserWindow, shell, ipcMain, nativeTheme, screen, powerMonitor, app } from 'electron' import { join } from 'path' import fs from 'fs' import { WORKING_DIR } from './constants'; @@ -45,7 +45,8 @@ export function createAppWindow(preloaderWindow?: BrowserWindow): void { nodeIntegrationInSubFrames: true, nodeIntegrationInWorker: true, webSecurity: false, - allowRunningInsecureContent: true + allowRunningInsecureContent: true, + autoplayPolicy: 'no-user-gesture-required' } }); @@ -73,6 +74,7 @@ export function createAppWindow(preloaderWindow?: BrowserWindow): void { } export function foundationIpcRegistration(mainWindow: BrowserWindow) { + let bounceId: number | null = null; ipcMain.removeAllListeners('window-resize'); ipcMain.removeAllListeners('window-resizeble'); ipcMain.removeAllListeners('window-theme'); @@ -86,6 +88,38 @@ export function foundationIpcRegistration(mainWindow: BrowserWindow) { ipcMain.removeHandler('window-minimize'); ipcMain.removeHandler('showItemInFolder'); ipcMain.removeHandler('openExternal'); + ipcMain.removeHandler('window-top'); + ipcMain.removeHandler('window-priority-normal'); + + ipcMain.handle('window-top', () => { + if (mainWindow.isMinimized()){ + mainWindow.restore(); + } + mainWindow.setAlwaysOnTop(true, "screen-saver"); // самый высокий уровень + mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + + mainWindow.show(); + mainWindow.focus(); + + if (process.platform === "darwin") { + /** + * Только в macos! Подпрыгивание иконки в Dock + */ + bounceId = app.dock!.bounce("critical"); + } + }) + + ipcMain.handle('window-priority-normal', () => { + mainWindow.setAlwaysOnTop(false); + mainWindow.setVisibleOnAllWorkspaces(false); + if(process.platform === "darwin" && bounceId !== null){ + /** + * Только в macos! Отмена подпрыгивания иконки в Dock + */ + app.dock!.cancelBounce(bounceId); + bounceId = null; + } + }) ipcMain.handle('open-dev-tools', () => { if (mainWindow.webContents.isDevToolsOpened()) { diff --git a/package.json b/package.json index 16e87de..985f4ec 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "Rosetta", - "version": "1.5.0", + "version": "1.5.2", "description": "Rosetta Messenger", "main": "./out/main/main.js", "license": "MIT", "build": { "electronUpdaterCompatibility": false, + "extraResources": [ + { "from": "resources/", "to": "resources/" } + ], "files": [ "node_modules/sqlite3/**/*", "out/main/**/*", From 61d55f266f66ee699943b6c777d55afdd1bb0df4 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 18 Mar 2026 19:56:39 +0200 Subject: [PATCH 44/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/version.ts b/app/version.ts index da72aec..92dfb77 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,9 +1,13 @@ -export const APP_VERSION = "1.0.8"; -export const CORE_MIN_REQUIRED_VERSION = "1.5.0"; +export const APP_VERSION = "1.1.0"; +export const CORE_MIN_REQUIRED_VERSION = "1.5.2"; export const RELEASE_NOTICE = ` -**Обновление v1.0.8** :emoji_1f631: -- Фикс проблемы с загрузкой аватарок в некоторых случаях -- Фикс фонового скролла при увеличении картинки -- Фикс артефактов у картинки +**Обновление v1.1.0** :emoji_1f631: +- Добавлена поддержка звонков +- Прозрачным аватаркам добавлена подложка +- Фикс ошибки чтения +- Подложка к вложению аватарки +- Обмен ключами шифрования DH +- Поддерджка WebRTC +- Событийные звуки звонка (сбросить, мутинг, и прочее...) `; \ No newline at end of file From 8f0e8e825136cbad7fd3de9502627fa77fc88cb8 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 18 Mar 2026 19:57:16 +0200 Subject: [PATCH 45/67] =?UTF-8?q?=D0=A3=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20PROD=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/servers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/servers.ts b/app/servers.ts index ce396d3..c5779c3 100644 --- a/app/servers.ts +++ b/app/servers.ts @@ -1,8 +1,8 @@ export const SERVERS = [ //'wss://cdn.rosetta-im.com', - 'ws://10.211.55.2:3000', + //'ws://10.211.55.2:3000', //'ws://192.168.6.82:3000', - //'wss://wss.rosetta.im' + 'wss://wss.rosetta.im' ]; export function selectServer(): string { From 9f8840e077bd6092b457fee5d02a8e363892f15a Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 16:46:23 +0200 Subject: [PATCH 46/67] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=B7=D0=B2?= =?UTF-8?q?=D1=83=D0=BA=D0=BE=D0=B2=20=D0=B8=D0=B7=20=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=83=D1=80=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/hooks/useSound.ts | 6 +++--- lib/main/ipcs/ipcRuntime.ts | 13 +++++++++++++ lib/main/main.ts | 1 + lib/preload/index.d.ts | 2 +- lib/preload/preload.ts | 13 +++++-------- 5 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 lib/main/ipcs/ipcRuntime.ts diff --git a/app/hooks/useSound.ts b/app/hooks/useSound.ts index 44ffac8..f4473e2 100644 --- a/app/hooks/useSound.ts +++ b/app/hooks/useSound.ts @@ -14,7 +14,7 @@ export function useSound() { audioRef.current.load(); }; - const playSound = (sound : string, loop: boolean = false) => { + const playSound = async (sound : string, loop: boolean = false) => { try { if(loop){ if (!loopingAudioRef.current) { @@ -24,7 +24,7 @@ export function useSound() { loopingAudioRef.current.loop = true; } - const url = window.mediaApi.getSoundUrl(sound); + const url = await window.mediaApi.getSoundUrl(sound); const player = loopingAudioRef.current; player.src = url; @@ -43,7 +43,7 @@ export function useSound() { audioRef.current.loop = loop; } - const url = window.mediaApi.getSoundUrl(sound); + const url = await window.mediaApi.getSoundUrl(sound); const player = audioRef.current; stopSound(); diff --git a/lib/main/ipcs/ipcRuntime.ts b/lib/main/ipcs/ipcRuntime.ts new file mode 100644 index 0000000..5c13ceb --- /dev/null +++ b/lib/main/ipcs/ipcRuntime.ts @@ -0,0 +1,13 @@ +import { app, ipcMain } from "electron"; +import path from "path"; + +/** + * Получить директорию с ресурсами приложения + */ +ipcMain.handle('runtime:get-resources', () => { + const isDev = !app.isPackaged && process.env['ELECTRON_RENDERER_URL']; + if(isDev){ + return path.join(process.cwd(), "resources") + } + return path.join(process.resourcesPath, "resources"); +}); \ No newline at end of file diff --git a/lib/main/main.ts b/lib/main/main.ts index 8a550ef..90c71db 100644 --- a/lib/main/main.ts +++ b/lib/main/main.ts @@ -8,6 +8,7 @@ import './ipcs/ipcUpdate' import './ipcs/ipcNotification' import './ipcs/ipcDevice' import './ipcs/ipcCore' +import './ipcs/ipcRuntime' import { Tray } from 'electron/main' import { join } from 'path' import { Logger } from './logger' diff --git a/lib/preload/index.d.ts b/lib/preload/index.d.ts index 284361b..ed3d8e1 100644 --- a/lib/preload/index.d.ts +++ b/lib/preload/index.d.ts @@ -14,7 +14,7 @@ declare global { deviceName: string; deviceId: string; mediaApi: { - getSoundUrl: (fileName: string) => string; + getSoundUrl: (fileName: string) => Promise; }; } } diff --git a/lib/preload/preload.ts b/lib/preload/preload.ts index dbe848a..288699a 100644 --- a/lib/preload/preload.ts +++ b/lib/preload/preload.ts @@ -6,12 +6,9 @@ import path from 'node:path' import fs from "node:fs"; -function resolveSound(fileName: string) { - const isDev = !process.env.APP_PACKAGED; // или свой флаг dev - const fullPath = isDev - ? path.join(process.cwd(), "resources", "sounds", fileName) - : path.join(process.resourcesPath, "resources", "sounds", fileName); - +async function resolveSound(fileName: string) { + const resourcesPath = await ipcRenderer.invoke('runtime:get-resources'); + const fullPath = path.join(resourcesPath, "sounds", fileName); if (!fs.existsSync(fullPath)) { throw new Error(`Sound not found: ${fullPath}`); } @@ -32,7 +29,7 @@ const exposeContext = async () => { } }); contextBridge.exposeInMainWorld("mediaApi", { - getSoundUrl: (fileName: string) => { + getSoundUrl: async (fileName: string) => { return resolveSound(fileName); } }); @@ -44,7 +41,7 @@ const exposeContext = async () => { window.api = api; window.shell = shell; window.mediaApi = { - getSoundUrl: (fileName: string) => { + getSoundUrl: async (fileName: string) => { return resolveSound(fileName); } } From 427f2e9e33566799bd3f3c1ca3928d9eb8035cfd Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 17:18:46 +0200 Subject: [PATCH 47/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20E2EE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 3 + app/providers/CallProvider/audioE2EE.ts | 76 +++++++++++++++++++++ tsconfig.web.json | 1 + 3 files changed, 80 insertions(+) create mode 100644 app/providers/CallProvider/audioE2EE.ts diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 36202f1..1367d79 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -12,6 +12,7 @@ import { modals } from "@mantine/modals"; import { Button, Flex, Text } from "@mantine/core"; import { useSound } from "@/app/hooks/useSound"; import useWindow from "@/app/hooks/useWindow"; +import { enableAudioE2EE } from "./audioE2EE"; export interface CallContextValue { call: (callable: string) => void; @@ -345,6 +346,8 @@ export function CallProvider(props : CallProviderProps) { localStream.getTracks().forEach(track => { peerConnectionRef.current?.addTrack(track, localStream); }); + + await enableAudioE2EE(peerConnectionRef.current, Buffer.from(sharedSecret, 'hex')); /** * Отправляем свой оффер другой стороне */ diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts new file mode 100644 index 0000000..a35b94d --- /dev/null +++ b/app/providers/CallProvider/audioE2EE.ts @@ -0,0 +1,76 @@ +function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer { + const u8 = src instanceof Uint8Array ? src : new Uint8Array(src); + return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; +} + +export async function enableAudioE2EE(pc: RTCPeerConnection, keyBuffer: Buffer | Uint8Array) { + const raw = toArrayBuffer(keyBuffer); + if (raw.byteLength !== 32) throw new Error("E2EE key must be 32 bytes"); + + const key = await crypto.subtle.importKey( + "raw", + raw, + { name: "AES-CTR" }, + false, + ["encrypt", "decrypt"] + ); + + const makeCounter = (ts: number, dir: number) => { + const counter = new Uint8Array(16); + const dv = new DataView(counter.buffer); + dv.setUint32(0, dir, false); // разделяем направление + dv.setBigUint64(8, BigInt(ts), false); // nonce из timestamp кадра + return counter; + }; + + const attachSender = (sender: RTCRtpSender) => { + // @ts-ignore Chromium/Electron API + if (!sender.createEncodedStreams) return; + // @ts-ignore + const { readable, writable } = sender.createEncodedStreams(); + + const enc = new TransformStream({ + async transform(frame: any, controller) { + const counter = makeCounter(frame.timestamp ?? 0, 1); + const out = await crypto.subtle.encrypt( + { name: "AES-CTR", counter, length: 64 }, + key, + frame.data + ); + frame.data = new Uint8Array(out); // тот же размер + controller.enqueue(frame); + } + }); + + readable.pipeThrough(enc).pipeTo(writable).catch(() => {}); + }; + + const attachReceiver = (receiver: RTCRtpReceiver) => { + // @ts-ignore Chromium/Electron API + if (!receiver.createEncodedStreams) return; + // @ts-ignore + const { readable, writable } = receiver.createEncodedStreams(); + + const dec = new TransformStream({ + async transform(frame: any, controller) { + const counter = makeCounter(frame.timestamp ?? 0, 1); + const out = await crypto.subtle.decrypt( + { name: "AES-CTR", counter, length: 64 }, + key, + frame.data + ); + frame.data = new Uint8Array(out); // тот же размер + controller.enqueue(frame); + } + }); + + readable.pipeThrough(dec).pipeTo(writable).catch(() => {}); + }; + + pc.getSenders().forEach((s) => s.track?.kind === "audio" && attachSender(s)); + pc.getReceivers().forEach((r) => r.track?.kind === "audio" && attachReceiver(r)); + + pc.addEventListener("track", (e) => { + if (e.track.kind === "audio") attachReceiver(e.receiver); + }); +} \ No newline at end of file diff --git a/tsconfig.web.json b/tsconfig.web.json index eb4c3b3..9b4503a 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,6 +5,7 @@ "composite": true, "jsx": "react-jsx", "baseUrl": ".", + "lib": ["DOM"], "esModuleInterop": true, "types": ["electron-vite/node"], "paths": { From d3cda685cd565652c00871252880c9ffd53c7eb5 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 17:26:52 +0200 Subject: [PATCH 48/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 22 ++-- app/providers/CallProvider/audioE2EE.ts | 135 ++++++++++++-------- 2 files changed, 96 insertions(+), 61 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 1367d79..db027ec 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -12,7 +12,7 @@ import { modals } from "@mantine/modals"; import { Button, Flex, Text } from "@mantine/core"; import { useSound } from "@/app/hooks/useSound"; import useWindow from "@/app/hooks/useWindow"; -import { enableAudioE2EE } from "./audioE2EE"; +import { attachReceiverE2EE, attachSenderE2EE } from "./audioE2EE"; export interface CallContextValue { call: (callable: string) => void; @@ -298,7 +298,9 @@ export function CallProvider(props : CallProviderProps) { * Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение */ peerConnectionRef.current = new RTCPeerConnection({ - iceServers: iceServersRef.current + iceServers: iceServersRef.current, + // @ts-ignore + encodedInsertableStreams: true }); /** * Подписываемся на ICE кандидат @@ -340,14 +342,18 @@ export function CallProvider(props : CallProviderProps) { * Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести, * когда мы установим WebRTC соединение */ - const localStream = await navigator.mediaDevices.getUserMedia({ - audio: true - }); - localStream.getTracks().forEach(track => { - peerConnectionRef.current?.addTrack(track, localStream); + const localStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const audioTrack = localStream.getAudioTracks()[0]; + + + const tx = peerConnectionRef.current.addTransceiver(audioTrack, { + direction: "sendrecv", + streams: [localStream] }); - await enableAudioE2EE(peerConnectionRef.current, Buffer.from(sharedSecret, 'hex')); + await attachSenderE2EE(tx.sender, Buffer.from(sharedSecret, "hex")); + + await attachReceiverE2EE(tx.receiver, Buffer.from(sharedSecret, "hex")); /** * Отправляем свой оффер другой стороне */ diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index a35b94d..0d385ba 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -3,74 +3,103 @@ function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer { return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; } -export async function enableAudioE2EE(pc: RTCPeerConnection, keyBuffer: Buffer | Uint8Array) { - const raw = toArrayBuffer(keyBuffer); - if (raw.byteLength !== 32) throw new Error("E2EE key must be 32 bytes"); +type KeyInput = Buffer | Uint8Array; - const key = await crypto.subtle.importKey( + +async function importAesCtrKey(input: KeyInput): Promise { + const keyBytes = toArrayBuffer(input); + if (keyBytes.byteLength !== 32) { + throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); + } + + return crypto.subtle.importKey( "raw", - raw, + keyBytes, { name: "AES-CTR" }, false, ["encrypt", "decrypt"] ); +} - const makeCounter = (ts: number, dir: number) => { - const counter = new Uint8Array(16); - const dv = new DataView(counter.buffer); - dv.setUint32(0, dir, false); // разделяем направление - dv.setBigUint64(8, BigInt(ts), false); // nonce из timestamp кадра - return counter; +function toBigIntTs(ts: unknown): bigint { + if (typeof ts === "bigint") return ts; + if (typeof ts === "number") return BigInt(ts); + return 0n; +} + +/** + * 16-byte counter: + * [0..3] direction marker + * [4..11] frame timestamp + * [12..15] reserved + */ +function buildCounter(direction: number, timestamp: unknown): ArrayBuffer { + const iv = new Uint8Array(16); + const dv = new DataView(iv.buffer); + dv.setUint32(0, direction >>> 0, false); + dv.setBigUint64(4, toBigIntTs(timestamp), false); + dv.setUint32(12, 0, false); + return toArrayBuffer(iv); +} + +export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise { + const key = await importAesCtrKey(keyInput); + + const anySender = sender as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; }; - const attachSender = (sender: RTCRtpSender) => { - // @ts-ignore Chromium/Electron API - if (!sender.createEncodedStreams) return; - // @ts-ignore - const { readable, writable } = sender.createEncodedStreams(); + if (!anySender.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpSender"); + } - const enc = new TransformStream({ - async transform(frame: any, controller) { - const counter = makeCounter(frame.timestamp ?? 0, 1); - const out = await crypto.subtle.encrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = new Uint8Array(out); // тот же размер - controller.enqueue(frame); - } - }); + const { readable, writable } = anySender.createEncodedStreams(); - readable.pipeThrough(enc).pipeTo(writable).catch(() => {}); + const enc = new TransformStream({ + async transform(frame, controller) { + const counter = buildCounter(1, frame.timestamp); + const encrypted = await crypto.subtle.encrypt( + { name: "AES-CTR", counter, length: 64 }, + key, + frame.data + ); + frame.data = encrypted; // same length + controller.enqueue(frame); + } + }); + + readable.pipeThrough(enc).pipeTo(writable).catch((e) => { + console.error("Sender E2EE pipeline failed:", e); + }); +} + +export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { + const key = await importAesCtrKey(keyInput); + + const anyReceiver = receiver as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; }; - const attachReceiver = (receiver: RTCRtpReceiver) => { - // @ts-ignore Chromium/Electron API - if (!receiver.createEncodedStreams) return; - // @ts-ignore - const { readable, writable } = receiver.createEncodedStreams(); + if (!anyReceiver.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); + } - const dec = new TransformStream({ - async transform(frame: any, controller) { - const counter = makeCounter(frame.timestamp ?? 0, 1); - const out = await crypto.subtle.decrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = new Uint8Array(out); // тот же размер - controller.enqueue(frame); - } - }); + const { readable, writable } = anyReceiver.createEncodedStreams(); - readable.pipeThrough(dec).pipeTo(writable).catch(() => {}); - }; + const dec = new TransformStream({ + async transform(frame, controller) { + const counter = buildCounter(1, frame.timestamp); + const decrypted = await crypto.subtle.decrypt( + { name: "AES-CTR", counter, length: 64 }, + key, + frame.data + ); + frame.data = decrypted; // same length + controller.enqueue(frame); + } + }); - pc.getSenders().forEach((s) => s.track?.kind === "audio" && attachSender(s)); - pc.getReceivers().forEach((r) => r.track?.kind === "audio" && attachReceiver(r)); - - pc.addEventListener("track", (e) => { - if (e.track.kind === "audio") attachReceiver(e.receiver); + readable.pipeThrough(dec).pipeTo(writable).catch((e) => { + console.error("Receiver E2EE pipeline failed:", e); }); } \ No newline at end of file From 59d40e3005b0e4e73186c1720b24ee838ed496dc Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:18:04 +0200 Subject: [PATCH 49/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index db027ec..62996bc 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -324,7 +324,12 @@ export function CallProvider(props : CallProviderProps) { } } - peerConnectionRef.current.ontrack = (event) => { + peerConnectionRef.current.ontrack = async (event) => { + try { + await attachReceiverE2EE(event.receiver, Buffer.from(sharedSecret, "hex")); + } catch (e) { + console.error("attachReceiverE2EE failed:", e); + } /** * При получении медиа-трека с другой стороны */ @@ -347,13 +352,12 @@ export function CallProvider(props : CallProviderProps) { const tx = peerConnectionRef.current.addTransceiver(audioTrack, { - direction: "sendrecv", - streams: [localStream] + direction: "sendrecv", + streams: [localStream] }); await attachSenderE2EE(tx.sender, Buffer.from(sharedSecret, "hex")); - await attachReceiverE2EE(tx.receiver, Buffer.from(sharedSecret, "hex")); /** * Отправляем свой оффер другой стороне */ @@ -365,7 +369,7 @@ export function CallProvider(props : CallProviderProps) { send(offerSignal); return; } - }, [activeCall, sessionKeys]); + }, [activeCall, sessionKeys, sharedSecret]); const openCallsModal = (text : string) => { modals.open({ From 46af6661a19422a2111d64faa2f9fac7eda5e304 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:22:49 +0200 Subject: [PATCH 50/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 21 +++++++++++---------- app/providers/CallProvider/audioE2EE.ts | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 62996bc..c0ee1d3 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -74,7 +74,8 @@ export function CallProvider(props : CallProviderProps) { const roomIdRef = useRef(""); const roleRef = useRef(null); - const [sharedSecret, setSharedSecret] = useState(""); + //const [sharedSecret, setSharedSecret] = useState(""); + const sharedSecretRef = useRef(""); const iceServersRef = useRef([]); const remoteAudioRef = useRef(null); const iceCandidatesBufferRef = useRef([]); @@ -246,8 +247,8 @@ export function CallProvider(props : CallProviderProps) { } const sessionKeys = generateSessionKeys(); const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); - setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); - info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); + sharedSecretRef.current = Buffer.from(computedSharedSecret).toString('hex'); + info("Generated shared secret for call session: " + sharedSecretRef.current); /** * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию */ @@ -284,8 +285,8 @@ export function CallProvider(props : CallProviderProps) { return; } const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); - info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); - setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); + sharedSecretRef.current = Buffer.from(computedSharedSecret).toString('hex'); + info("Generated shared secret for call session: " + sharedSecretRef.current); setCallState(CallState.WEB_RTC_EXCHANGE); } if(signalType == SignalType.CREATE_ROOM) { @@ -326,7 +327,7 @@ export function CallProvider(props : CallProviderProps) { peerConnectionRef.current.ontrack = async (event) => { try { - await attachReceiverE2EE(event.receiver, Buffer.from(sharedSecret, "hex")); + await attachReceiverE2EE(event.receiver, Buffer.from(sharedSecretRef.current, "hex")); } catch (e) { console.error("attachReceiverE2EE failed:", e); } @@ -356,7 +357,7 @@ export function CallProvider(props : CallProviderProps) { streams: [localStream] }); - await attachSenderE2EE(tx.sender, Buffer.from(sharedSecret, "hex")); + await attachSenderE2EE(tx.sender, Buffer.from(sharedSecretRef.current, "hex")); /** * Отправляем свой оффер другой стороне @@ -369,7 +370,7 @@ export function CallProvider(props : CallProviderProps) { send(offerSignal); return; } - }, [activeCall, sessionKeys, sharedSecret]); + }, [activeCall, sessionKeys]); const openCallsModal = (text : string) => { modals.open({ @@ -483,10 +484,10 @@ export function CallProvider(props : CallProviderProps) { * @returns */ const getKeyCast = () => { - if(!sharedSecret){ + if(!sharedSecretRef.current){ return ""; } - return sharedSecret; + return sharedSecretRef.current; } diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index 0d385ba..97fbd6e 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -7,6 +7,7 @@ type KeyInput = Buffer | Uint8Array; async function importAesCtrKey(input: KeyInput): Promise { +console.info("Importing AES-CTR key for E2EE:", Buffer.from(input).toString('hex')); const keyBytes = toArrayBuffer(input); if (keyBytes.byteLength !== 32) { throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); From f91392e6aa78befa859f1cddcb4c8b85921c439f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:30:07 +0200 Subject: [PATCH 51/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5,=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= =?UTF-8?q?=20E2EE=20=D0=B8=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D0=BC=D0=B8=20=D1=81=20DH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/audioE2EE.ts | 141 ++++++++++++------------ 1 file changed, 70 insertions(+), 71 deletions(-) diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index 97fbd6e..216d065 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -1,31 +1,30 @@ function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer { - const u8 = src instanceof Uint8Array ? src : new Uint8Array(src); - return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; + const u8 = src instanceof Uint8Array ? src : new Uint8Array(src); + return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; } type KeyInput = Buffer | Uint8Array; async function importAesCtrKey(input: KeyInput): Promise { -console.info("Importing AES-CTR key for E2EE:", Buffer.from(input).toString('hex')); - const keyBytes = toArrayBuffer(input); - if (keyBytes.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); - } + const keyBytes = toArrayBuffer(input); + if (keyBytes.byteLength !== 32) { + throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); + } - return crypto.subtle.importKey( - "raw", - keyBytes, - { name: "AES-CTR" }, - false, - ["encrypt", "decrypt"] - ); + return crypto.subtle.importKey( + "raw", + keyBytes, + { name: "AES-CTR" }, + false, + ["encrypt", "decrypt"] + ); } function toBigIntTs(ts: unknown): bigint { - if (typeof ts === "bigint") return ts; - if (typeof ts === "number") return BigInt(ts); - return 0n; + if (typeof ts === "bigint") return ts; + if (typeof ts === "number") return BigInt(ts); + return 0n; } /** @@ -35,72 +34,72 @@ function toBigIntTs(ts: unknown): bigint { * [12..15] reserved */ function buildCounter(direction: number, timestamp: unknown): ArrayBuffer { - const iv = new Uint8Array(16); - const dv = new DataView(iv.buffer); - dv.setUint32(0, direction >>> 0, false); - dv.setBigUint64(4, toBigIntTs(timestamp), false); - dv.setUint32(12, 0, false); - return toArrayBuffer(iv); + const iv = new Uint8Array(16); + const dv = new DataView(iv.buffer); + dv.setUint32(0, direction >>> 0, false); + dv.setBigUint64(4, toBigIntTs(timestamp), false); + dv.setUint32(12, 0, false); + return toArrayBuffer(iv); } export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise { - const key = await importAesCtrKey(keyInput); + const key = await importAesCtrKey(keyInput); - const anySender = sender as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const anySender = sender as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; + }; - if (!anySender.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpSender"); - } - - const { readable, writable } = anySender.createEncodedStreams(); - - const enc = new TransformStream({ - async transform(frame, controller) { - const counter = buildCounter(1, frame.timestamp); - const encrypted = await crypto.subtle.encrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = encrypted; // same length - controller.enqueue(frame); + if (!anySender.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpSender"); } - }); - readable.pipeThrough(enc).pipeTo(writable).catch((e) => { - console.error("Sender E2EE pipeline failed:", e); - }); + const { readable, writable } = anySender.createEncodedStreams(); + + const enc = new TransformStream({ + async transform(frame, controller) { + const counter = buildCounter(1, frame.timestamp); + const encrypted = await crypto.subtle.encrypt( + { name: "AES-CTR", counter, length: 64 }, + key, + frame.data + ); + frame.data = encrypted; // same length + controller.enqueue(frame); + } + }); + + readable.pipeThrough(enc).pipeTo(writable).catch((e) => { + console.error("Sender E2EE pipeline failed:", e); + }); } export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { - const key = await importAesCtrKey(keyInput); + const key = await importAesCtrKey(keyInput); - const anyReceiver = receiver as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const anyReceiver = receiver as unknown as { + createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; + }; - if (!anyReceiver.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); - } - - const { readable, writable } = anyReceiver.createEncodedStreams(); - - const dec = new TransformStream({ - async transform(frame, controller) { - const counter = buildCounter(1, frame.timestamp); - const decrypted = await crypto.subtle.decrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = decrypted; // same length - controller.enqueue(frame); + if (!anyReceiver.createEncodedStreams) { + throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); } - }); - readable.pipeThrough(dec).pipeTo(writable).catch((e) => { - console.error("Receiver E2EE pipeline failed:", e); - }); + const { readable, writable } = anyReceiver.createEncodedStreams(); + + const dec = new TransformStream({ + async transform(frame, controller) { + const counter = buildCounter(1, frame.timestamp); + const decrypted = await crypto.subtle.decrypt( + { name: "AES-CTR", counter, length: 64 }, + key, + frame.data + ); + frame.data = decrypted; // same length + controller.enqueue(frame); + } + }); + + readable.pipeThrough(dec).pipeTo(writable).catch((e) => { + console.error("Receiver E2EE pipeline failed:", e); + }); } \ No newline at end of file From e5a4c92ba7cb0653e9b6e10103bf7e334d0c794f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 18:48:11 +0200 Subject: [PATCH 52/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 15 +++++---------- package.json | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/version.ts b/app/version.ts index 92dfb77..1bf51b8 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,13 +1,8 @@ -export const APP_VERSION = "1.1.0"; -export const CORE_MIN_REQUIRED_VERSION = "1.5.2"; +export const APP_VERSION = "1.1.1"; +export const CORE_MIN_REQUIRED_VERSION = "1.5.3"; export const RELEASE_NOTICE = ` -**Обновление v1.1.0** :emoji_1f631: -- Добавлена поддержка звонков -- Прозрачным аватаркам добавлена подложка -- Фикс ошибки чтения -- Подложка к вложению аватарки -- Обмен ключами шифрования DH -- Поддерджка WebRTC -- Событийные звуки звонка (сбросить, мутинг, и прочее...) +**Обновление v1.1.1** :emoji_1f631: +- Добавлено сквозное шифрование звонков +- Исправлена проблема с звуком в звонках на некоторых устройствах `; \ No newline at end of file diff --git a/package.json b/package.json index 985f4ec..9332cd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Rosetta", - "version": "1.5.2", + "version": "1.5.3", "description": "Rosetta Messenger", "main": "./out/main/main.js", "license": "MIT", From 5032d92f8ef9df59aa6740da9cc0c76b710d66ac Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 21:24:36 +0200 Subject: [PATCH 53/67] =?UTF-8?q?WASM=20=D1=83=D1=81=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=B0=D0=BB=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D0=BC=20=D1=88=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=B7=D0=B1?= =?UTF-8?q?=D0=B5=D0=B6=D0=B0=D0=BD=D0=B8=D1=8F=20backpressure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/audioE2EE.ts | 117 ++++++++++++------------ package.json | 7 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index 216d065..fb6ed29 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -1,54 +1,46 @@ -function toArrayBuffer(src: Buffer | Uint8Array): ArrayBuffer { - const u8 = src instanceof Uint8Array ? src : new Uint8Array(src); - return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; -} +import { chacha20 } from "@noble/ciphers/chacha"; type KeyInput = Buffer | Uint8Array; +const senderAttached = new WeakSet(); +const receiverAttached = new WeakSet(); -async function importAesCtrKey(input: KeyInput): Promise { - const keyBytes = toArrayBuffer(input); - if (keyBytes.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${keyBytes.byteLength}`); - } - - return crypto.subtle.importKey( - "raw", - keyBytes, - { name: "AES-CTR" }, - false, - ["encrypt", "decrypt"] - ); +function toUint8Array(input: KeyInput): Uint8Array { + const u8 = input instanceof Uint8Array ? input : new Uint8Array(input); + return new Uint8Array(u8.slice().buffer); } -function toBigIntTs(ts: unknown): bigint { - if (typeof ts === "bigint") return ts; - if (typeof ts === "number") return BigInt(ts); - return 0n; +function buildNonce(timestamp: unknown): Uint8Array { + const nonce = new Uint8Array(12); + const ts = typeof timestamp === "number" + ? timestamp + : typeof timestamp === "bigint" + ? Number(timestamp) + : 0; + new DataView(nonce.buffer).setUint32(8, ts >>> 0, false); + return nonce; } -/** - * 16-byte counter: - * [0..3] direction marker - * [4..11] frame timestamp - * [12..15] reserved - */ -function buildCounter(direction: number, timestamp: unknown): ArrayBuffer { - const iv = new Uint8Array(16); - const dv = new DataView(iv.buffer); - dv.setUint32(0, direction >>> 0, false); - dv.setBigUint64(4, toBigIntTs(timestamp), false); - dv.setUint32(12, 0, false); - return toArrayBuffer(iv); +function processFrame(data: ArrayBuffer, key: Uint8Array, timestamp: unknown): ArrayBuffer { + const nonce = buildNonce(timestamp); + const input = new Uint8Array(data); + // ChaCha20 симметричный: encrypt === decrypt, тот же размер + const output = chacha20(key, nonce, input); + return output.buffer as ArrayBuffer; } export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise { - const key = await importAesCtrKey(keyInput); + if (senderAttached.has(sender)) { + return; + } + senderAttached.add(sender); - const anySender = sender as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const key = toUint8Array(keyInput); + if (key.byteLength !== 32) { + throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); + } + const anySender = sender as any; if (!anySender.createEncodedStreams) { throw new Error("createEncodedStreams is not available on RTCRtpSender"); } @@ -56,15 +48,15 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput) const { readable, writable } = anySender.createEncodedStreams(); const enc = new TransformStream({ - async transform(frame, controller) { - const counter = buildCounter(1, frame.timestamp); - const encrypted = await crypto.subtle.encrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = encrypted; // same length - controller.enqueue(frame); + // Синхронный transform — нет async, нет накопления очереди + transform(frame, controller) { + try { + frame.data = processFrame(frame.data, key, frame.timestamp); + controller.enqueue(frame); + } catch (e) { + console.error("Sender E2EE frame failed:", e); + controller.enqueue(frame); + } } }); @@ -74,12 +66,17 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput) } export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { - const key = await importAesCtrKey(keyInput); + if (receiverAttached.has(receiver)) { + return; + } + receiverAttached.add(receiver); - const anyReceiver = receiver as unknown as { - createEncodedStreams?: () => { readable: ReadableStream; writable: WritableStream }; - }; + const key = toUint8Array(keyInput); + if (key.byteLength !== 32) { + throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); + } + const anyReceiver = receiver as any; if (!anyReceiver.createEncodedStreams) { throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); } @@ -87,15 +84,15 @@ export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: Key const { readable, writable } = anyReceiver.createEncodedStreams(); const dec = new TransformStream({ - async transform(frame, controller) { - const counter = buildCounter(1, frame.timestamp); - const decrypted = await crypto.subtle.decrypt( - { name: "AES-CTR", counter, length: 64 }, - key, - frame.data - ); - frame.data = decrypted; // same length - controller.enqueue(frame); + // Синхронный transform — нет async, нет накопления очереди + transform(frame, controller) { + try { + frame.data = processFrame(frame.data, key, frame.timestamp); + controller.enqueue(frame); + } catch (e) { + console.error("Receiver E2EE frame failed:", e); + controller.enqueue(frame); + } } }); diff --git a/package.json b/package.json index 9332cd4..06f0e09 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "build": { "electronUpdaterCompatibility": false, "extraResources": [ - { "from": "resources/", "to": "resources/" } + { + "from": "resources/", + "to": "resources/" + } ], "files": [ "node_modules/sqlite3/**/*", @@ -81,7 +84,7 @@ "@mantine/form": "^8.3.12", "@mantine/hooks": "^8.3.12", "@mantine/modals": "^8.3.12", - "@noble/ciphers": "^1.2.1", + "@noble/ciphers": "^1.3.0", "@noble/secp256k1": "^3.0.0", "@tabler/icons-react": "^3.31.0", "@types/crypto-js": "^4.2.2", From f269046c46bedf4bd5194d8618f7f2180497a95d Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 20 Mar 2026 22:25:33 +0200 Subject: [PATCH 54/67] =?UTF-8?q?=D0=9E=D1=82=D0=BB=D0=B0=D0=B4=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B8=D0=B7=D0=B2=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/audioE2EE.ts | 115 +++++++++++------------- 1 file changed, 51 insertions(+), 64 deletions(-) diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index fb6ed29..fd8f81f 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -10,93 +10,80 @@ function toUint8Array(input: KeyInput): Uint8Array { return new Uint8Array(u8.slice().buffer); } -function buildNonce(timestamp: unknown): Uint8Array { +/** + * Переиспользуемый процессор фреймов. + * Один экземпляр на sender/receiver — нет аллокаций на каждый фрейм. + */ +function createFrameProcessor(key: Uint8Array) { + // Переиспользуемые буферы — не создаём новые на каждый фрейм const nonce = new Uint8Array(12); - const ts = typeof timestamp === "number" - ? timestamp - : typeof timestamp === "bigint" - ? Number(timestamp) - : 0; - new DataView(nonce.buffer).setUint32(8, ts >>> 0, false); - return nonce; + const nonceView = new DataView(nonce.buffer); + + return function processFrame(data: ArrayBuffer): ArrayBuffer { + // Переиспользуем nonce буфер + nonce.fill(0); + nonceView.setUint32(8, (data.byteLength ^ (data.byteLength << 8)) >>> 0, false); + + const input = new Uint8Array(data); + const output = chacha20(key, nonce, input); + return output.buffer as ArrayBuffer; + }; } -function processFrame(data: ArrayBuffer, key: Uint8Array, timestamp: unknown): ArrayBuffer { - const nonce = buildNonce(timestamp); - const input = new Uint8Array(data); - // ChaCha20 симметричный: encrypt === decrypt, тот же размер - const output = chacha20(key, nonce, input); - return output.buffer as ArrayBuffer; +function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) { + return new TransformStream({ + transform(frame, controller) { + try { + const start = performance.now(); + frame.data = processFrame(frame.data); + const elapsed = performance.now() - start; + if (elapsed > 1) { + console.warn(`E2EE slow frame: ${elapsed.toFixed(2)}ms`); + } + controller.enqueue(frame); + } catch (e) { + // не рвём поток — пропускаем фрейм как есть + console.error("E2EE frame failed:", e); + controller.enqueue(frame); + } + } + }); } export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise { - if (senderAttached.has(sender)) { - return; - } + if (senderAttached.has(sender)) return; senderAttached.add(sender); const key = toUint8Array(keyInput); - if (key.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); - } + if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); const anySender = sender as any; - if (!anySender.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpSender"); - } + if (!anySender.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpSender"); const { readable, writable } = anySender.createEncodedStreams(); + const processFrame = createFrameProcessor(key); - const enc = new TransformStream({ - // Синхронный transform — нет async, нет накопления очереди - transform(frame, controller) { - try { - frame.data = processFrame(frame.data, key, frame.timestamp); - controller.enqueue(frame); - } catch (e) { - console.error("Sender E2EE frame failed:", e); - controller.enqueue(frame); - } - } - }); - - readable.pipeThrough(enc).pipeTo(writable).catch((e) => { - console.error("Sender E2EE pipeline failed:", e); - }); + readable + .pipeThrough(createTransform(processFrame)) + .pipeTo(writable) + .catch((e) => console.error("Sender E2EE pipeline failed:", e)); } export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { - if (receiverAttached.has(receiver)) { - return; - } + if (receiverAttached.has(receiver)) return; receiverAttached.add(receiver); const key = toUint8Array(keyInput); - if (key.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); - } + if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); const anyReceiver = receiver as any; - if (!anyReceiver.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); - } + if (!anyReceiver.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpReceiver"); const { readable, writable } = anyReceiver.createEncodedStreams(); + const processFrame = createFrameProcessor(key); - const dec = new TransformStream({ - // Синхронный transform — нет async, нет накопления очереди - transform(frame, controller) { - try { - frame.data = processFrame(frame.data, key, frame.timestamp); - controller.enqueue(frame); - } catch (e) { - console.error("Receiver E2EE frame failed:", e); - controller.enqueue(frame); - } - } - }); - - readable.pipeThrough(dec).pipeTo(writable).catch((e) => { - console.error("Receiver E2EE pipeline failed:", e); - }); + readable + .pipeThrough(createTransform(processFrame)) + .pipeTo(writable) + .catch((e) => console.error("Receiver E2EE pipeline failed:", e)); } \ No newline at end of file From 4df39cb83d54474e5c93052d7a8f90d1a7179cd2 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 18:51:39 +0200 Subject: [PATCH 55/67] =?UTF-8?q?WASM=20=D0=B4=D0=BB=D1=8F=20=D1=83=D1=81?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=88=D0=B8=D1=84?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B7=D0=B2=D0=BE?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=D0=B2,=20=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 61 +++++++++++++ app/providers/CallProvider/audioE2EE.ts | 99 +++++++++++++++------ package.json | 2 + 3 files changed, 133 insertions(+), 29 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index c0ee1d3..8c3f7a5 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -98,6 +98,67 @@ export function CallProvider(props : CallProviderProps) { } }, [callState]); + // ...existing code... +const checkWebRTCStats = async () => { + if (!peerConnectionRef.current) return; + + const stats = await peerConnectionRef.current.getStats(); + + stats.forEach(report => { + // Исходящий аудио + if (report.type === "outbound-rtp" && report.mediaType === "audio") { + console.info("[WebRTC OUT]", { + bytesSent: report.bytesSent, + packetsSent: report.packetsSent, + timestamp: report.timestamp + }); + } + + // Входящий аудио + if (report.type === "inbound-rtp" && report.mediaType === "audio") { + console.info("[WebRTC IN]", { + bytesReceived: report.bytesReceived, + packetsReceived: report.packetsReceived, + jitter: report.jitter, + packetsLost: report.packetsLost, + timestamp: report.timestamp + }); + } + + // RTT и задержка + if (report.type === "candidate-pair" && report.state === "succeeded") { + console.info("[WebRTC RTT]", { + currentRoundTripTime: (report.currentRoundTripTime * 1000).toFixed(2) + "ms", + availableOutgoingBitrate: (report.availableOutgoingBitrate / 1024 / 1024).toFixed(2) + " Mbps", + availableIncomingBitrate: (report.availableIncomingBitrate / 1024 / 1024).toFixed(2) + " Mbps" + }); + } + + // Codec info + if (report.type === "codec") { + if (report.mediaType === "audio") { + console.info("[WebRTC Codec]", { + mimeType: report.mimeType, + channels: report.channels, + clockRate: report.clockRate + }); + } + } + }); +}; + +// Вызываем каждые 2 секунды при активном звонке +useEffect(() => { + if (callState !== CallState.ACTIVE) return; + + const interval = setInterval(() => { + void checkWebRTCStats(); + }, 2000); + + return () => clearInterval(interval); +}, [callState]); +// ...existing code... + useEffect(() => { /** * Нам нужно получить ICE серверы для установки соединения из разных сетей diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index fd8f81f..df2b51a 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -1,51 +1,80 @@ -import { chacha20 } from "@noble/ciphers/chacha"; +import _sodium from "libsodium-wrappers"; type KeyInput = Buffer | Uint8Array; const senderAttached = new WeakSet(); const receiverAttached = new WeakSet(); +let sodiumReady = false; +let sodium: typeof _sodium; + +export async function initE2EE(): Promise { + if (sodiumReady) return; + await _sodium.ready; + sodium = _sodium; + sodiumReady = true; +} + function toUint8Array(input: KeyInput): Uint8Array { const u8 = input instanceof Uint8Array ? input : new Uint8Array(input); return new Uint8Array(u8.slice().buffer); } -/** - * Переиспользуемый процессор фреймов. - * Один экземпляр на sender/receiver — нет аллокаций на каждый фрейм. - */ function createFrameProcessor(key: Uint8Array) { - // Переиспользуемые буферы — не создаём новые на каждый фрейм - const nonce = new Uint8Array(12); - const nonceView = new DataView(nonce.buffer); + // Переиспользуемый nonce буфер — нет аллокаций на каждый фрейм + const nonce = new Uint8Array(sodium.crypto_stream_chacha20_NONCEBYTES); // 8 bytes return function processFrame(data: ArrayBuffer): ArrayBuffer { - // Переиспользуем nonce буфер - nonce.fill(0); - nonceView.setUint32(8, (data.byteLength ^ (data.byteLength << 8)) >>> 0, false); - const input = new Uint8Array(data); - const output = chacha20(key, nonce, input); + + // Обновляем nonce из длины и byteOffset фрейма — уникально и без аллокаций + nonce.fill(0); + new DataView(nonce.buffer).setUint32(0, input.byteLength ^ 0xdeadbeef, false); + new DataView(nonce.buffer).setUint32(4, input[0] ^ input[input.byteLength - 1], false); + + // WASM ChaCha20 — синхронный, нативная скорость + const output = sodium.crypto_stream_chacha20_xor(input, nonce, key); return output.buffer as ArrayBuffer; }; } function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) { + let frames = 0; + let slowFrames = 0; + let total = 0; + let max = 0; + return new TransformStream({ transform(frame, controller) { + const started = performance.now(); + try { - const start = performance.now(); frame.data = processFrame(frame.data); - const elapsed = performance.now() - start; - if (elapsed > 1) { - console.warn(`E2EE slow frame: ${elapsed.toFixed(2)}ms`); - } - controller.enqueue(frame); } catch (e) { - // не рвём поток — пропускаем фрейм как есть - console.error("E2EE frame failed:", e); + console.error("[E2EE] frame error:", e); controller.enqueue(frame); + return; } + + const elapsed = performance.now() - started; + frames++; + total += elapsed; + if (elapsed > max) max = elapsed; + if (elapsed > 2) slowFrames++; + + if (frames >= 200) { + console.info("[E2EE stats]", { + avgMs: +(total / frames).toFixed(3), + maxMs: +max.toFixed(3), + slowFrames, + }); + frames = 0; + slowFrames = 0; + total = 0; + max = 0; + } + + controller.enqueue(frame); } }); } @@ -54,36 +83,48 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput) if (senderAttached.has(sender)) return; senderAttached.add(sender); + await initE2EE(); + const key = toUint8Array(keyInput); - if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); + if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) { + throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`); + } const anySender = sender as any; - if (!anySender.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpSender"); + if (!anySender.createEncodedStreams) { + throw new Error("createEncodedStreams not available on RTCRtpSender"); + } const { readable, writable } = anySender.createEncodedStreams(); - const processFrame = createFrameProcessor(key); + const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES)); readable .pipeThrough(createTransform(processFrame)) .pipeTo(writable) - .catch((e) => console.error("Sender E2EE pipeline failed:", e)); + .catch((e) => console.error("[E2EE] Sender pipeline failed:", e)); } export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { if (receiverAttached.has(receiver)) return; receiverAttached.add(receiver); + await initE2EE(); + const key = toUint8Array(keyInput); - if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); + if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) { + throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`); + } const anyReceiver = receiver as any; - if (!anyReceiver.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpReceiver"); + if (!anyReceiver.createEncodedStreams) { + throw new Error("createEncodedStreams not available on RTCRtpReceiver"); + } const { readable, writable } = anyReceiver.createEncodedStreams(); - const processFrame = createFrameProcessor(key); + const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES)); readable .pipeThrough(createTransform(processFrame)) .pipeTo(writable) - .catch((e) => console.error("Receiver E2EE pipeline failed:", e)); + .catch((e) => console.error("[E2EE] Receiver pipeline failed:", e)); } \ No newline at end of file diff --git a/package.json b/package.json index 06f0e09..6518075 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "jsencrypt": "^3.3.2", "jszip": "^3.10.1", "libsodium": "^0.8.2", + "libsodium-wrappers": "^0.8.2", "lottie-react": "^2.4.1", "node-forge": "^1.3.1", "node-machine-id": "^1.1.12", @@ -136,6 +137,7 @@ "@electron/rebuild": "^4.0.3", "@rushstack/eslint-patch": "^1.10.5", "@tailwindcss/vite": "^4.0.9", + "@types/libsodium-wrappers": "^0.7.14", "@types/node": "^22.13.5", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", From 0d70824d77fe8a1561f9d6445e9aacf218e022c5 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 19:03:43 +0200 Subject: [PATCH 56/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/audioE2EE.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index df2b51a..7de4900 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -21,24 +21,23 @@ function toUint8Array(input: KeyInput): Uint8Array { } function createFrameProcessor(key: Uint8Array) { - // Переиспользуемый nonce буфер — нет аллокаций на каждый фрейм + // Выделяем nonce один раз — переиспользуем на каждый фрейм const nonce = new Uint8Array(sodium.crypto_stream_chacha20_NONCEBYTES); // 8 bytes + const nonceView = new DataView(nonce.buffer, 0, nonce.byteLength); - return function processFrame(data: ArrayBuffer): ArrayBuffer { + return function processFrame(data: ArrayBuffer, timestamp: number): ArrayBuffer { const input = new Uint8Array(data); - // Обновляем nonce из длины и byteOffset фрейма — уникально и без аллокаций - nonce.fill(0); - new DataView(nonce.buffer).setUint32(0, input.byteLength ^ 0xdeadbeef, false); - new DataView(nonce.buffer).setUint32(4, input[0] ^ input[input.byteLength - 1], false); + // Безопасно записываем nonce через отдельный DataView + nonceView.setUint32(0, (timestamp >>> 0) & 0xffffffff, false); + nonceView.setUint32(4, ((timestamp / 0x100000000) >>> 0) & 0xffffffff, false); - // WASM ChaCha20 — синхронный, нативная скорость const output = sodium.crypto_stream_chacha20_xor(input, nonce, key); return output.buffer as ArrayBuffer; }; } -function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) { +function createTransform(processFrame: (data: ArrayBuffer, timestamp: number) => ArrayBuffer) { let frames = 0; let slowFrames = 0; let total = 0; @@ -49,7 +48,9 @@ function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) { const started = performance.now(); try { - frame.data = processFrame(frame.data); + // Передаём timestamp фрейма как nonce + const ts = typeof frame.timestamp === "number" ? frame.timestamp : 0; + frame.data = processFrame(frame.data, ts); } catch (e) { console.error("[E2EE] frame error:", e); controller.enqueue(frame); From 1d6c30fb08b88e15a82108701e12514fa116770c Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 19:16:44 +0200 Subject: [PATCH 57/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/audioE2EE.ts | 86 +++++++++++-------------- package.json | 2 +- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index 7de4900..b7505d6 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -1,4 +1,4 @@ -import _sodium from "libsodium-wrappers"; +import _sodium from "libsodium-wrappers-sumo"; type KeyInput = Buffer | Uint8Array; @@ -17,64 +17,48 @@ export async function initE2EE(): Promise { function toUint8Array(input: KeyInput): Uint8Array { const u8 = input instanceof Uint8Array ? input : new Uint8Array(input); - return new Uint8Array(u8.slice().buffer); + return new Uint8Array(u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength)); +} + +function fillNonceFromTimestamp(nonce: Uint8Array, tsRaw: unknown): void { + nonce.fill(0); + + let ts = 0n; + if (typeof tsRaw === "bigint") ts = tsRaw; + else if (typeof tsRaw === "number" && Number.isFinite(tsRaw)) ts = BigInt(Math.floor(tsRaw)); + + // Записываем 64-bit timestamp в первые 8 байт nonce (BE) + nonce[0] = Number((ts >> 56n) & 0xffn); + nonce[1] = Number((ts >> 48n) & 0xffn); + nonce[2] = Number((ts >> 40n) & 0xffn); + nonce[3] = Number((ts >> 32n) & 0xffn); + nonce[4] = Number((ts >> 24n) & 0xffn); + nonce[5] = Number((ts >> 16n) & 0xffn); + nonce[6] = Number((ts >> 8n) & 0xffn); + nonce[7] = Number(ts & 0xffn); } function createFrameProcessor(key: Uint8Array) { - // Выделяем nonce один раз — переиспользуем на каждый фрейм - const nonce = new Uint8Array(sodium.crypto_stream_chacha20_NONCEBYTES); // 8 bytes - const nonceView = new DataView(nonce.buffer, 0, nonce.byteLength); + const nonceLen = sodium.crypto_stream_xchacha20_NONCEBYTES; // 24 + const nonce = new Uint8Array(nonceLen); - return function processFrame(data: ArrayBuffer, timestamp: number): ArrayBuffer { + return function processFrame(data: ArrayBuffer, timestamp: unknown): ArrayBuffer { const input = new Uint8Array(data); + fillNonceFromTimestamp(nonce, timestamp); - // Безопасно записываем nonce через отдельный DataView - nonceView.setUint32(0, (timestamp >>> 0) & 0xffffffff, false); - nonceView.setUint32(4, ((timestamp / 0x100000000) >>> 0) & 0xffffffff, false); - - const output = sodium.crypto_stream_chacha20_xor(input, nonce, key); - return output.buffer as ArrayBuffer; + const output = sodium.crypto_stream_xchacha20_xor(input, nonce, key); + return output.buffer.slice(output.byteOffset, output.byteOffset + output.byteLength) as ArrayBuffer; }; } -function createTransform(processFrame: (data: ArrayBuffer, timestamp: number) => ArrayBuffer) { - let frames = 0; - let slowFrames = 0; - let total = 0; - let max = 0; - +function createTransform(processFrame: (data: ArrayBuffer, timestamp: unknown) => ArrayBuffer) { return new TransformStream({ transform(frame, controller) { - const started = performance.now(); - try { - // Передаём timestamp фрейма как nonce - const ts = typeof frame.timestamp === "number" ? frame.timestamp : 0; - frame.data = processFrame(frame.data, ts); + frame.data = processFrame(frame.data, frame.timestamp); } catch (e) { console.error("[E2EE] frame error:", e); - controller.enqueue(frame); - return; } - - const elapsed = performance.now() - started; - frames++; - total += elapsed; - if (elapsed > max) max = elapsed; - if (elapsed > 2) slowFrames++; - - if (frames >= 200) { - console.info("[E2EE stats]", { - avgMs: +(total / frames).toFixed(3), - maxMs: +max.toFixed(3), - slowFrames, - }); - frames = 0; - slowFrames = 0; - total = 0; - max = 0; - } - controller.enqueue(frame); } }); @@ -87,8 +71,9 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput) await initE2EE(); const key = toUint8Array(keyInput); - if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) { - throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`); + const keyLen = sodium.crypto_stream_xchacha20_KEYBYTES; // 32 + if (key.byteLength < keyLen) { + throw new Error(`Key must be at least ${keyLen} bytes`); } const anySender = sender as any; @@ -97,7 +82,7 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput) } const { readable, writable } = anySender.createEncodedStreams(); - const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES)); + const processFrame = createFrameProcessor(key.slice(0, keyLen)); readable .pipeThrough(createTransform(processFrame)) @@ -112,8 +97,9 @@ export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: Key await initE2EE(); const key = toUint8Array(keyInput); - if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) { - throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`); + const keyLen = sodium.crypto_stream_xchacha20_KEYBYTES; // 32 + if (key.byteLength < keyLen) { + throw new Error(`Key must be at least ${keyLen} bytes`); } const anyReceiver = receiver as any; @@ -122,7 +108,7 @@ export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: Key } const { readable, writable } = anyReceiver.createEncodedStreams(); - const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES)); + const processFrame = createFrameProcessor(key.slice(0, keyLen)); readable .pipeThrough(createTransform(processFrame)) diff --git a/package.json b/package.json index 6518075..7728df2 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "@mantine/form": "^8.3.12", "@mantine/hooks": "^8.3.12", "@mantine/modals": "^8.3.12", - "@noble/ciphers": "^1.3.0", "@noble/secp256k1": "^3.0.0", "@tabler/icons-react": "^3.31.0", "@types/crypto-js": "^4.2.2", @@ -112,6 +111,7 @@ "jszip": "^3.10.1", "libsodium": "^0.8.2", "libsodium-wrappers": "^0.8.2", + "libsodium-wrappers-sumo": "^0.8.2", "lottie-react": "^2.4.1", "node-forge": "^1.3.1", "node-machine-id": "^1.1.12", From 0c823c398f217464aa3e8d660c7b65d82589ac53 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 19:21:56 +0200 Subject: [PATCH 58/67] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7728df2..1b152fb 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@mantine/form": "^8.3.12", "@mantine/hooks": "^8.3.12", "@mantine/modals": "^8.3.12", + "@noble/ciphers": "^1.3.0", "@noble/secp256k1": "^3.0.0", "@tabler/icons-react": "^3.31.0", "@types/crypto-js": "^4.2.2", From 98fbabc130b7340fa7f09350d839e015ba10605b Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 19:37:52 +0200 Subject: [PATCH 59/67] =?UTF-8?q?=D0=A4=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D1=87=D0=B0=D1=81=D1=82=D1=8C=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8=20=D1=88?= =?UTF-8?q?=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=BE=D0=B2=20(frames)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/CallProvider/CallProvider.tsx | 64 +-------------------- app/providers/CallProvider/audioE2EE.ts | 3 - 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 8c3f7a5..598a41d 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -74,8 +74,7 @@ export function CallProvider(props : CallProviderProps) { const roomIdRef = useRef(""); const roleRef = useRef(null); - //const [sharedSecret, setSharedSecret] = useState(""); - const sharedSecretRef = useRef(""); + const sharedSecretRef = useRef(""); const iceServersRef = useRef([]); const remoteAudioRef = useRef(null); const iceCandidatesBufferRef = useRef([]); @@ -98,67 +97,6 @@ export function CallProvider(props : CallProviderProps) { } }, [callState]); - // ...existing code... -const checkWebRTCStats = async () => { - if (!peerConnectionRef.current) return; - - const stats = await peerConnectionRef.current.getStats(); - - stats.forEach(report => { - // Исходящий аудио - if (report.type === "outbound-rtp" && report.mediaType === "audio") { - console.info("[WebRTC OUT]", { - bytesSent: report.bytesSent, - packetsSent: report.packetsSent, - timestamp: report.timestamp - }); - } - - // Входящий аудио - if (report.type === "inbound-rtp" && report.mediaType === "audio") { - console.info("[WebRTC IN]", { - bytesReceived: report.bytesReceived, - packetsReceived: report.packetsReceived, - jitter: report.jitter, - packetsLost: report.packetsLost, - timestamp: report.timestamp - }); - } - - // RTT и задержка - if (report.type === "candidate-pair" && report.state === "succeeded") { - console.info("[WebRTC RTT]", { - currentRoundTripTime: (report.currentRoundTripTime * 1000).toFixed(2) + "ms", - availableOutgoingBitrate: (report.availableOutgoingBitrate / 1024 / 1024).toFixed(2) + " Mbps", - availableIncomingBitrate: (report.availableIncomingBitrate / 1024 / 1024).toFixed(2) + " Mbps" - }); - } - - // Codec info - if (report.type === "codec") { - if (report.mediaType === "audio") { - console.info("[WebRTC Codec]", { - mimeType: report.mimeType, - channels: report.channels, - clockRate: report.clockRate - }); - } - } - }); -}; - -// Вызываем каждые 2 секунды при активном звонке -useEffect(() => { - if (callState !== CallState.ACTIVE) return; - - const interval = setInterval(() => { - void checkWebRTCStats(); - }, 2000); - - return () => clearInterval(interval); -}, [callState]); -// ...existing code... - useEffect(() => { /** * Нам нужно получить ICE серверы для установки соединения из разных сетей diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index b7505d6..4a8de6c 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -22,12 +22,9 @@ function toUint8Array(input: KeyInput): Uint8Array { function fillNonceFromTimestamp(nonce: Uint8Array, tsRaw: unknown): void { nonce.fill(0); - let ts = 0n; if (typeof tsRaw === "bigint") ts = tsRaw; else if (typeof tsRaw === "number" && Number.isFinite(tsRaw)) ts = BigInt(Math.floor(tsRaw)); - - // Записываем 64-bit timestamp в первые 8 байт nonce (BE) nonce[0] = Number((ts >> 56n) & 0xffn); nonce[1] = Number((ts >> 48n) & 0xffn); nonce[2] = Number((ts >> 40n) & 0xffn); From 48e0cddbaad2319e52f1a6d270136629392519c6 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 19:39:47 +0200 Subject: [PATCH 60/67] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=BE=D0=B2=20=D1=81=D0=B0=D0=BC=D0=BE=D0=BC?= =?UTF-8?q?=D1=83=20=D1=81=D0=B5=D0=B1=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ChatHeader/ChatHeader.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/ChatHeader/ChatHeader.tsx b/app/components/ChatHeader/ChatHeader.tsx index f14023e..000877a 100644 --- a/app/components/ChatHeader/ChatHeader.tsx +++ b/app/components/ChatHeader/ChatHeader.tsx @@ -135,11 +135,13 @@ export function ChatHeader() { - call(dialog)} style={{ cursor: 'pointer' }} stroke={1.5} color={theme.colors.blue[7]} size={24}> + )} Date: Sat, 21 Mar 2026 21:18:09 +0200 Subject: [PATCH 61/67] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=82?= =?UTF-8?q?=D0=B8=D0=BF=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20-=20Attachment.CALL=20=D1=81=20=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=B2=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageAttachments/MessageAttachments.tsx | 3 + .../MessageAttachments/MessageCall.tsx | 62 +++++++ .../usePrepareAttachment.ts | 148 ++++++++++++++++ app/providers/CallProvider/CallProvider.tsx | 26 +++ .../DialogProvider/DialogProvider.tsx | 146 +++++----------- .../DialogProvider/useDeattachedSender.ts | 160 ++++++++++++++++++ app/providers/DialogProvider/useDialog.ts | 7 +- .../DialogProvider/useDialogsCache.ts | 2 +- .../protocol/packets/packet.message.ts | 3 +- .../constructLastMessageTextByAttachments.ts | 2 + 10 files changed, 446 insertions(+), 113 deletions(-) create mode 100644 app/components/MessageAttachments/MessageCall.tsx create mode 100644 app/providers/AttachmentProvider/usePrepareAttachment.ts create mode 100644 app/providers/DialogProvider/useDeattachedSender.ts 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]"; } From 91b955d621fd706c72f07e43bf9c732c5cd206c3 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 21:28:20 +0200 Subject: [PATCH 62/67] =?UTF-8?q?=D0=A1=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=BE=D0=B2=20=D0=B2=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/AttachmentProvider/usePrepareAttachment.ts | 6 ++++++ app/providers/CallProvider/CallProvider.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/providers/AttachmentProvider/usePrepareAttachment.ts b/app/providers/AttachmentProvider/usePrepareAttachment.ts index dc940bd..bb198fb 100644 --- a/app/providers/AttachmentProvider/usePrepareAttachment.ts +++ b/app/providers/AttachmentProvider/usePrepareAttachment.ts @@ -101,6 +101,12 @@ export function usePrepareAttachment() { try{ for(let i = 0; i < attachments.length; i++){ const attachment : Attachment = attachments[i]; + if(attachment.type == AttachmentType.CALL){ + /** + * Звонок загружать не надо + */ + continue; + } if(attachment.type == AttachmentType.MESSAGES){ let reply : MessageReply[] = JSON.parse(attachment.blob) for(let j = 0; j < reply.length; j++){ diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index c0afb4c..bf1d342 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -373,7 +373,7 @@ export function CallProvider(props : CallProviderProps) { send(offerSignal); return; } - }, [activeCall, sessionKeys]); + }, [activeCall, sessionKeys, duration]); const openCallsModal = (text : string) => { modals.open({ From 6f95f326bf37eed014c202ac6c739817a108df52 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 21:30:39 +0200 Subject: [PATCH 63/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 12 +++++++----- package.json | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/version.ts b/app/version.ts index 1bf51b8..37f3032 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,8 +1,10 @@ -export const APP_VERSION = "1.1.1"; -export const CORE_MIN_REQUIRED_VERSION = "1.5.3"; +export const APP_VERSION = "1.1.2"; +export const CORE_MIN_REQUIRED_VERSION = "1.5.4"; export const RELEASE_NOTICE = ` -**Обновление v1.1.1** :emoji_1f631: -- Добавлено сквозное шифрование звонков -- Исправлена проблема с звуком в звонках на некоторых устройствах +**Обновление v1.1.2** :emoji_1f631: +- Улучшено шифрование звонков, теперь они более производительне и стабильные. +- Добавлены события звонков (начало, окончание, пропущенные). +- Улучшена организация кода. +- Исправлены мелкие баги и улучшена стабильность приложения. `; \ No newline at end of file diff --git a/package.json b/package.json index 1b152fb..bd8e2eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Rosetta", - "version": "1.5.3", + "version": "1.5.4", "description": "Rosetta Messenger", "main": "./out/main/main.js", "license": "MIT", From bd3411de52295b2e910f3ad63b76c45ac6503326 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 21:31:57 +0200 Subject: [PATCH 64/67] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.ts b/app/version.ts index 37f3032..098454c 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,5 +1,5 @@ export const APP_VERSION = "1.1.2"; -export const CORE_MIN_REQUIRED_VERSION = "1.5.4"; +export const CORE_MIN_REQUIRED_VERSION = "1.5.3"; export const RELEASE_NOTICE = ` **Обновление v1.1.2** :emoji_1f631: diff --git a/package.json b/package.json index bd8e2eb..1b152fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Rosetta", - "version": "1.5.4", + "version": "1.5.3", "description": "Rosetta Messenger", "main": "./out/main/main.js", "license": "MIT", From 329e6d7825ca3d57c290d5ca4e9364fd9bc1336f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sat, 21 Mar 2026 21:33:24 +0200 Subject: [PATCH 65/67] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20CI/CD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/darwin.yaml | 1 + .gitea/workflows/linux.yaml | 1 + .gitea/workflows/service-packs.yaml | 2 +- .gitea/workflows/windows.yaml | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/darwin.yaml b/.gitea/workflows/darwin.yaml index 8cf6332..9261fe1 100644 --- a/.gitea/workflows/darwin.yaml +++ b/.gitea/workflows/darwin.yaml @@ -1,4 +1,5 @@ name: MacOS Kernel Build +run-name: Build and Upload MacOS Kernel #Запускаем только кнопкой "Run workflow" в Actions -> Build MacOS on: diff --git a/.gitea/workflows/linux.yaml b/.gitea/workflows/linux.yaml index a8aab9b..57b2ee5 100644 --- a/.gitea/workflows/linux.yaml +++ b/.gitea/workflows/linux.yaml @@ -1,4 +1,5 @@ name: Linux Kernel Build +run-name: Build and Upload Linux Kernel #Запускаем только кнопкой "Run workflow" в Actions on: diff --git a/.gitea/workflows/service-packs.yaml b/.gitea/workflows/service-packs.yaml index 9ce0ca3..b0e3864 100644 --- a/.gitea/workflows/service-packs.yaml +++ b/.gitea/workflows/service-packs.yaml @@ -1,6 +1,6 @@ name: SP Builds +run-name: Build and Upload SP Packages -#Запускаем только кнопкой "Run workflow" в Actions -> Build MacOS on: workflow_dispatch: push: diff --git a/.gitea/workflows/windows.yaml b/.gitea/workflows/windows.yaml index e4095df..ef7ba32 100644 --- a/.gitea/workflows/windows.yaml +++ b/.gitea/workflows/windows.yaml @@ -1,4 +1,5 @@ name: Windows Kernel Build +run-name: Build and Upload Windows Kernel #Запускаем только кнопкой "Run workflow" в Actions -> Build Windows #Или если есть коммпит в папку lib в ветке main From b300fa4d0384bd26bbd4aec5dcfa9d3e81bd3b30 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sun, 22 Mar 2026 16:08:24 +0200 Subject: [PATCH 66/67] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=20CI/CD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/darwin.yaml | 50 ++++++++++++--------- .gitea/workflows/linux.yaml | 38 +++++++--------- .gitea/workflows/service-packs.yaml | 50 ++++++++++++++------- .gitea/workflows/sshupload.sh | 67 ----------------------------- 4 files changed, 80 insertions(+), 125 deletions(-) delete mode 100644 .gitea/workflows/sshupload.sh diff --git a/.gitea/workflows/darwin.yaml b/.gitea/workflows/darwin.yaml index 9261fe1..9c067f7 100644 --- a/.gitea/workflows/darwin.yaml +++ b/.gitea/workflows/darwin.yaml @@ -13,6 +13,10 @@ on: jobs: build: runs-on: macos + strategy: + fail-fast: false + matrix: + arch: [x64, arm64] steps: - name: Checkout code uses: actions/checkout@v6 @@ -31,6 +35,7 @@ jobs: restore-keys: | ${{ runner.os }}-npm- if-no-files-found: ignore + # Кэш для electron-builder - name: Cache electron-builder uses: actions/cache@v5 @@ -42,32 +47,35 @@ jobs: restore-keys: | ${{ runner.os }}-electron-builder- if-no-files-found: ignore + - name: NPM offline setup shell: bash run: | npm config set cache "$HOME/.npm-cache" --global npm config set prefer-offline true --global + - name: Install npm dependencies run: npm install --prefer-offline --no-audit --no-fund - - name: Build the application - run: npm run kernel:mac - #Загружаем на удаленный сервер по SSH используя scp и пароль из секретов - #Загружаем из двух папок dist/builds/darwin/x64 и dist/builds/darwin/arm64, так как electron-builder может создавать разные файлы для разных архитектур - #Вызываем файл sshupload.sh и передаем ему параметры из секретов, чтобы не хранить пароль в открытом виде в workflow - - name: Upload to SSH using scp - shell: bash + + - name: Build the application (${{ matrix.arch }}) run: | - chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" - sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \ - -l "$GITHUB_WORKSPACE/dist/builds/darwin/x64/Rosetta-*.pkg" \ - -r "${{ secrets.SDU_SSH_KERNEL }}/darwin/x64" \ - -s "${{ secrets.SDU_SSH_HOST }}" \ - -u "${{ secrets.SDU_SSH_USERNAME }}" \ - -p '${{ secrets.SDU_SSH_PASSWORD }}' - sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \ - -l "$GITHUB_WORKSPACE/dist/builds/darwin/arm64/Rosetta-*.pkg" \ - -r "${{ secrets.SDU_SSH_KERNEL }}/darwin/arm64" \ - -s "${{ secrets.SDU_SSH_HOST }}" \ - -u "${{ secrets.SDU_SSH_USERNAME }}" \ - -p '${{ secrets.SDU_SSH_PASSWORD }}' - \ No newline at end of file + npx electron-vite build + npx electron-builder --mac --${{ matrix.arch }} + + - name: Check if files exist (${{ matrix.arch }}) + run: | + echo "=== Checking dist structure ===" + find dist/builds/darwin/${{ matrix.arch }} -type f -name "*.pkg" 2>/dev/null || echo "No PKG files found" + ls -la dist/builds/darwin/${{ matrix.arch }}/ 2>/dev/null || echo "arch folder not found" + + - name: Upload ${{ matrix.arch }} to SSH using SCP + uses: appleboy/scp-action@master + with: + host: ${{ secrets.SDU_SSH_HOST }} + username: ${{ secrets.SDU_SSH_USERNAME }} + password: ${{ secrets.SDU_SSH_PASSWORD }} + port: 22 + source: "dist/builds/darwin/${{ matrix.arch }}/Rosetta-*.pkg" + target: "${{ secrets.SDU_SSH_KERNEL }}/darwin/${{ matrix.arch }}" + strip_components: 4 + rm: true \ No newline at end of file diff --git a/.gitea/workflows/linux.yaml b/.gitea/workflows/linux.yaml index 57b2ee5..9c4abdd 100644 --- a/.gitea/workflows/linux.yaml +++ b/.gitea/workflows/linux.yaml @@ -12,7 +12,11 @@ on: jobs: build: - runs-on: linux + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + arch: [x64, arm64] steps: - name: Checkout code uses: actions/checkout@v6 @@ -49,38 +53,28 @@ jobs: - name: Install npm dependencies run: npm install --no-audit --no-fund - - name: Build the application - run: npm run kernel:linux + - name: Build the application (${{ matrix.arch }}) + run: | + npx electron-vite build + npx electron-builder --linux --${{ matrix.arch }} - - name: Check if files exist + - name: Check if files exist (${{ matrix.arch }}) run: | echo "=== Checking dist structure ===" - find dist/builds -type f -name "*.AppImage" 2>/dev/null || echo "No AppImage files found" - ls -la dist/builds/linux/ 2>/dev/null || echo "linux folder not found" + find dist/builds/linux/${{ matrix.arch }} -type f -name "*.AppImage" 2>/dev/null || echo "No AppImage files found" + ls -la dist/builds/linux/${{ matrix.arch }}/ 2>/dev/null || echo "arch folder not found" - name: Install SCP in Docker container run: apt-get install -y openssh-client - - name: Upload x64 to SSH using SCP + - name: Upload ${{ matrix.arch }} to SSH using SCP uses: appleboy/scp-action@master with: host: ${{ secrets.SDU_SSH_HOST }} username: ${{ secrets.SDU_SSH_USERNAME }} password: ${{ secrets.SDU_SSH_PASSWORD }} port: 22 - source: "dist/builds/linux/x64/Rosetta-*.AppImage" - target: "${{ secrets.SDU_SSH_KERNEL }}/linux/x64" + source: "dist/builds/linux/${{ matrix.arch }}/Rosetta-*.AppImage" + target: "${{ secrets.SDU_SSH_KERNEL }}/linux/${{ matrix.arch }}" strip_components: 4 - rm: true - - - name: Upload arm64 to SSH using SCP - uses: appleboy/scp-action@master - with: - host: ${{ secrets.SDU_SSH_HOST }} - username: ${{ secrets.SDU_SSH_USERNAME }} - password: ${{ secrets.SDU_SSH_PASSWORD }} - port: 22 - source: "dist/builds/linux/arm64/Rosetta-*.AppImage" - target: "${{ secrets.SDU_SSH_KERNEL }}/linux/arm64" - strip_components: 4 - rm: true \ No newline at end of file + rm: true \ No newline at end of file diff --git a/.gitea/workflows/service-packs.yaml b/.gitea/workflows/service-packs.yaml index b0e3864..fd1b0d7 100644 --- a/.gitea/workflows/service-packs.yaml +++ b/.gitea/workflows/service-packs.yaml @@ -11,7 +11,7 @@ on: jobs: build: - runs-on: macos + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 @@ -29,13 +29,13 @@ jobs: restore-keys: | ${{ runner.os }}-npm- if-no-files-found: ignore - # Кэш для electron-builder (macOS) - - name: Cache electron-builder (macOS) + # Кэш для electron-builder (Linux) + - name: Cache electron-builder (Linux) uses: actions/cache@v5 with: path: | - ${{ env.HOME }}/Library/Caches/electron-builder - ${{ env.HOME }}/Library/Caches/electron + ${{ env.HOME }}/.cache/electron-builder + ${{ env.HOME }}/.cache/electron key: ${{ runner.os }}-electron-builder-${{ hashFiles('**/electron-builder.yml') }} restore-keys: | ${{ runner.os }}-electron-builder- @@ -54,6 +54,11 @@ jobs: - name: Build the application run: npm run kernel:linux + - name: Install ZIP in Docker container + run: | + apt-get update + apt-get install -y zip + #Собираем сервисные пакеты для всех платформ - name: Build SP shell: bash @@ -62,13 +67,28 @@ jobs: sh "$GITHUB_WORKSPACE/build-packs.sh" #Загружаем на удаленный сервер по SSH используя scp и пароль из секретов #Загружаем из двух папок dist/builds/darwin/x64 и dist/builds/darwin/arm64, так как electron-builder может создавать разные файлы для разных архитектур - - name: Upload to SSH - shell: bash - run: | - chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" - sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \ - -l "$GITHUB_WORKSPACE/packs/*" \ - -r "${{ secrets.SDU_SSH_PACKS }}" \ - -s "${{ secrets.SDU_SSH_HOST }}" \ - -u "${{ secrets.SDU_SSH_USERNAME }}" \ - -p '${{ secrets.SDU_SSH_PASSWORD }}' \ No newline at end of file + # - name: Upload to SSH + # shell: bash + # run: | + # chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" + # sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \ + # -l "$GITHUB_WORKSPACE/packs/*" \ + # -r "${{ secrets.SDU_SSH_PACKS }}" \ + # -s "${{ secrets.SDU_SSH_HOST }}" \ + # -u "${{ secrets.SDU_SSH_USERNAME }}" \ + # -p '${{ secrets.SDU_SSH_PASSWORD }}' + + - name: Install SCP in Docker container + run: apt-get install -y openssh-client + + - name: Upload ${{ matrix.arch }} to SSH using SCP + uses: appleboy/scp-action@master + with: + host: ${{ secrets.SDU_SSH_HOST }} + username: ${{ secrets.SDU_SSH_USERNAME }} + password: ${{ secrets.SDU_SSH_PASSWORD }} + port: 22 + source: "$GITHUB_WORKSPACE/packs/*" + target: "${{ secrets.SDU_SSH_PACKS }}" + strip_components: 1 + rm: true \ No newline at end of file diff --git a/.gitea/workflows/sshupload.sh b/.gitea/workflows/sshupload.sh deleted file mode 100644 index 9ecf450..0000000 --- a/.gitea/workflows/sshupload.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: sshupload.sh -l -r -s -u -p -EOF -} - -local_glob="" -remote_dir="" -server="" -user="" -password="" - -while [[ $# -gt 0 ]]; do - case "$1" in - -l|--local) local_glob="$2"; shift 2;; - -r|--remote) remote_dir="$2"; shift 2;; - -s|--server) server="$2"; shift 2;; - -u|--user) user="$2"; shift 2;; - -p|--password) password="$2"; shift 2;; - -h|--help) usage; exit 0;; - *) echo "Unknown arg: $1" >&2; usage; exit 1;; - esac -done - -if [[ -z "$local_glob" || -z "$remote_dir" || -z "$server" || -z "$user" || -z "$password" ]]; then - echo "Missing required params" >&2 - usage - exit 1 -fi - -# Ensure sshpass installed -if ! command -v sshpass >/dev/null 2>&1; then - if command -v brew >/dev/null 2>&1; then - brew update - brew install hudochenkov/sshpass/sshpass - elif command -v apt-get >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y sshpass - else - echo "sshpass not found and no supported package manager" >&2 - exit 1 - fi -fi - -user_host="${user}@${server}" - -# Ensure remote dir exists and clear it -sshpass -p "$password" ssh -o StrictHostKeyChecking=no "$user_host" "mkdir -p '$remote_dir' && rm -f '$remote_dir'/*" - -# Expand glob (supports ~ and patterns) and upload each file (compatible with macOS bash 3.x) -shopt -s nullglob -eval "files=( ${local_glob} )" -shopt -u nullglob - -if [[ ${#files[@]} -eq 0 ]]; then - echo "No files matched: $local_glob" >&2 - exit 1 -fi - -for f in "${files[@]}"; do - sshpass -p "$password" scp -o StrictHostKeyChecking=no "$f" "$user_host:$remote_dir/" -done - -echo "Upload completed" From 426f0c40bcf0228fe001cceeb08d6a1081776a10 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sun, 22 Mar 2026 16:41:09 +0200 Subject: [PATCH 67/67] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B1=D0=BB=D0=B5?= =?UTF-8?q?=D0=BC=D0=B0=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D0=BC=D0=B8=20=D0=B2=20=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- problems/problem_calls.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 problems/problem_calls.md diff --git a/problems/problem_calls.md b/problems/problem_calls.md new file mode 100644 index 0000000..a039c10 --- /dev/null +++ b/problems/problem_calls.md @@ -0,0 +1,10 @@ +# Проблема обмена ключами в звонках + +Для того, чтобы звонки работали защищенно, необходимо обмениваться ключами между участниками. При этом, сам ключ не должен передаваться по сети, иначе его смогут перехватить злоумышленники. Поэтому, для обмена ключами используется специальный протокол, который позволяет участникам обмениваться ключами без передачи их по сети. В Rosetta можно было использовать уже известный и проверенный контур шифрования по которому шифруются сообщения с использованием публичных и приватных ключей. Однако реализация такого метода может обернуться проблемами при эксплуатации, так как при ответе на звонок, участник может не иметь доступа к приватному ключу, который используется для дешифровки сообщений (например, при ответе на звонок с телефона, когда в приложение не был совершен вход, то есть оно выгружено из памяти), в этом случае звонок не будет работать, так как участник не сможет дешифровать голос и видео из звонка. + +## Возможное решение +Можно заставлять пользователя входить в приложение при ответе на звонок, чтобы он мог получить доступ к приватному ключу и дешифровать звонок. Однако, это может привести к неудобствам для пользователей, так как им придется каждый раз входить в приложение при ответе на звонок, что может быть особенно проблематично при использовании мобильного устройства. + +## Решение использованное в Rosetta +Для решения проблемы обмена ключами в звонках, в Rosetta используется алгоритм Диффи-Хеллмана для генерации общего секрета между участниками звонка. Этот алгоритм позволяет участникам обмениваться публичными ключами и генерировать общий секрет, который используется для шифрования и дешифрования медиа-потока в звонке. При этом, это не требует входа в приложение, так как ключ генерируется случайный при каждом звонке, и не зависит от приватного ключа. Это обеспечивает удобство для пользователей, так как им не нужно входить в приложение при ответе на звонок, и при этом обеспечивает безопасность звонков, так как ключи не передаются по сети и генерируются случайным образом для каждого звонка. Таким образом, Rosetta обеспечивает безопасные и удобные звонки для пользователей. +