Промежуточный этап синхронизации

This commit is contained in:
RoyceDa
2026-02-15 14:56:44 +02:00
parent 40ff99e66d
commit 8b906169ce
15 changed files with 609 additions and 427 deletions

View File

@@ -31,7 +31,7 @@ export function ChatHeader() {
const theme = useMantineTheme(); const theme = useMantineTheme();
const [blocked, blockUser, unblockUser] = useBlacklist(dialog); const [blocked, blockUser, unblockUser] = useBlacklist(dialog);
const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog); const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog);
const protocolState = useProtocolState(); const [protocolState] = useProtocolState();
const [userTypeing, setUserTypeing] = useState(false); const [userTypeing, setUserTypeing] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>(undefined); const timeoutRef = useRef<NodeJS.Timeout>(undefined);
const avatars = useAvatars(dialog); const avatars = useAvatars(dialog);

View File

@@ -0,0 +1,39 @@
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider";
import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState";
import { Flex, Loader, Text } from "@mantine/core";
export function DialogHeaderText() {
const [protocolState] = useProtocolState();
const colors = useRosettaColors();
const headerType = () => {
switch(protocolState){
case ProtocolState.SYNCHRONIZATION:
return (<>
<Loader size={12} color={colors.chevrons.active}></Loader>
<Text fw={500} style={{
userSelect: 'none'
}} size={'sm'}>Updating...</Text>
</>);
case ProtocolState.CONNECTED:
return (<>
<Text fw={500} style={{
userSelect: 'none'
}} size={'sm'}>Chats</Text>
</>);
default:
return (<>
<Text fw={500} style={{
userSelect: 'none'
}} size={'sm'}>Chats</Text>
</>);
}
}
return (
<Flex direction={'row'} align={'center'} gap={'xs'}>
{headerType()}
</Flex>
)
}

View File

@@ -6,6 +6,7 @@ import { useLogout } from "@/app/providers/AccountProvider/useLogout";
import { useHotkeys } from "@mantine/hooks"; import { useHotkeys } from "@mantine/hooks";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey"; import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
import { DialogHeaderText } from "../DialogHeaderText/DialogHeaderText";
export function DialogsPanelHeader() { export function DialogsPanelHeader() {
const colors = useRosettaColors(); const colors = useRosettaColors();
@@ -66,9 +67,7 @@ export function DialogsPanelHeader() {
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
<Text fw={500} style={{ <DialogHeaderText></DialogHeaderText>
userSelect: 'none'
}} size={'sm'}>Chats</Text>
<Menu withArrow width={150} shadow="md"> <Menu withArrow width={150} shadow="md">
<Menu.Target> <Menu.Target>
<IconEdit style={{ <IconEdit style={{

View File

@@ -25,7 +25,7 @@ export function GroupHeader() {
const {deleteMessages, dialog} = useDialog(); const {deleteMessages, dialog} = useDialog();
const theme = useMantineTheme(); const theme = useMantineTheme();
const {groupInfo} = useGroupInformation(dialog); const {groupInfo} = useGroupInformation(dialog);
const protocolState = useProtocolState(); const [protocolState] = useProtocolState();
const [usersTypeing, setUsersTypeing] = useState<{ const [usersTypeing, setUsersTypeing] = useState<{
timeout: NodeJS.Timeout | null, timeout: NodeJS.Timeout | null,
fromPublicKey: string fromPublicKey: string

View File

@@ -8,7 +8,7 @@ import { MacFrameButtons } from "../MacFrameButtons/MacFrameButtons";
export function Topbar() { export function Topbar() {
const colors = useRosettaColors(); const colors = useRosettaColors();
const protocolState = useProtocolState(); const [protocolState] = useProtocolState();
return ( return (
@@ -16,14 +16,14 @@ export function Topbar() {
{window.platform == 'win32' && <WindowsFrameButtons></WindowsFrameButtons>} {window.platform == 'win32' && <WindowsFrameButtons></WindowsFrameButtons>}
{window.platform == 'darwin' && <MacFrameButtons></MacFrameButtons>} {window.platform == 'darwin' && <MacFrameButtons></MacFrameButtons>}
{window.platform == 'linux' && <WindowsFrameButtons></WindowsFrameButtons>} {window.platform == 'linux' && <WindowsFrameButtons></WindowsFrameButtons>}
{(protocolState == ProtocolState.CONNECTED || !window.location.hash.includes("main")) && {(protocolState == ProtocolState.CONNECTED || protocolState == ProtocolState.SYNCHRONIZATION || !window.location.hash.includes("main")) &&
<Flex align={'center'} justify={'center'}> <Flex align={'center'} justify={'center'}>
<Text fw={'bolder'} fz={13} c={'gray'}> <Text fw={'bolder'} fz={13} c={'gray'}>
Rosetta Messenger Rosetta Messenger
</Text> </Text>
</Flex> </Flex>
} }
{(protocolState != ProtocolState.CONNECTED && protocolState != ProtocolState.DEVICE_VERIFICATION_REQUIRED && window.location.hash.includes("main")) && {(protocolState != ProtocolState.CONNECTED && protocolState != ProtocolState.SYNCHRONIZATION && protocolState != ProtocolState.DEVICE_VERIFICATION_REQUIRED && window.location.hash.includes("main")) &&
<Flex align={'center'} gap={5} justify={'center'}> <Flex align={'center'} gap={5} justify={'center'}>
<Loader size={12} color={colors.chevrons.active}></Loader> <Loader size={12} color={colors.chevrons.active}></Loader>
<Text fw={'bolder'} fz={13} c={'gray'}> <Text fw={'bolder'} fz={13} c={'gray'}>

View File

@@ -0,0 +1,12 @@
let tail: Promise<void> = Promise.resolve();
export const runTaskInQueue = (fn: () => Promise<void>) => {
tail = tail.then(fn).catch((e) => {
console.error("Dialog queue error", e);
});
};
/**
* Ждет, пока все пакеты попадающие в очередь не будут обработаны
*/
export const whenFinish = () => tail;

View File

@@ -44,7 +44,7 @@ export function useDialog() : {
const privateKey = usePrivateKeyHash(); const privateKey = usePrivateKeyHash();
const privatePlain = usePrivatePlain(); const privatePlain = usePrivatePlain();
const {writeFile} = useFileStorage(); const {writeFile} = useFileStorage();
const protocolState = useProtocolState(); const [protocolState] = useProtocolState();
const {hasGroup, getGroupKey} = useGroups(); const {hasGroup, getGroupKey} = useGroups();
const {warn} = useConsoleLogger('useDialog'); const {warn} = useConsoleLogger('useDialog');

View File

@@ -24,6 +24,7 @@ import { useGroups } from "./useGroups";
import { useDialogState } from "../DialogStateProvider.tsx/useDialogState"; import { useDialogState } from "../DialogStateProvider.tsx/useDialogState";
import { useUserInformation } from "../InformationProvider/useUserInformation"; import { useUserInformation } from "../InformationProvider/useUserInformation";
import { useMentions } from "../DialogStateProvider.tsx/useMentions"; import { useMentions } from "../DialogStateProvider.tsx/useMentions";
import { runTaskInQueue } from "./dialogQueue";
/** /**
* При вызове будет запущен "фоновый" обработчик * При вызове будет запущен "фоновый" обработчик
@@ -41,14 +42,14 @@ export function useDialogFiber() {
const notify = useNotification(); const notify = useNotification();
const focused = useWindowFocus(); const focused = useWindowFocus();
const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache(); const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache();
const {info, error} = useConsoleLogger('useDialogFiber'); const { info, error } = useConsoleLogger('useDialogFiber');
const [viewState] = useViewPanelsState(); const [viewState] = useViewPanelsState();
const {writeFile} = useFileStorage(); const { writeFile } = useFileStorage();
const {updateDialog} = useDialogsList(); const { updateDialog } = useDialogsList();
const {hasGroup, getGroupKey, normalize} = useGroups(); const { hasGroup, getGroupKey, normalize } = useGroups();
const {muted} = useDialogState(); const { muted } = useDialogState();
const [userInfo] = useUserInformation(publicKey); const [userInfo] = useUserInformation(publicKey);
const {pushMention} = useMentions(); const { pushMention } = useMentions();
/** /**
* Лог * Лог
@@ -64,486 +65,501 @@ export function useDialogFiber() {
* Метод нужен для синхронизации своих сообщений * Метод нужен для синхронизации своих сообщений
*/ */
usePacket(0x06, async (packet: PacketMessage) => { usePacket(0x06, async (packet: PacketMessage) => {
const fromPublicKey = packet.getFromPublicKey(); runTaskInQueue(async () => {
const toPublicKey = packet.getToPublicKey(); const fromPublicKey = packet.getFromPublicKey();
const aesChachaKey = packet.getAesChachaKey(); const toPublicKey = packet.getToPublicKey();
const content = packet.getContent(); const aesChachaKey = packet.getAesChachaKey();
const timestamp = packet.getTimestamp(); const content = packet.getContent();
const messageId = packet.getMessageId(); const timestamp = packet.getTimestamp();
const messageId = packet.getMessageId();
if(fromPublicKey != publicKey){ if (fromPublicKey != publicKey) {
/**
* Игнорируем если это не сообщение от нас
*/
return;
}
const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary");
const key = chachaDecryptedKey.slice(0, 32);
const nonce = chachaDecryptedKey.slice(32);
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
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); return;
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, }
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary");
messageAttachments[nextLength - 1].blob = decryptedBlob; const key = chachaDecryptedKey.slice(0, 32);
const nonce = chachaDecryptedKey.slice(32);
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
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
});
} }
attachmentsMeta.push({ const newMessage: Message = {
id: attachment.id, from_public_key: fromPublicKey,
type: attachment.type, to_public_key: toPublicKey,
preview: attachment.preview 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
};
const newMessage: Message = { await runQuery(`
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 INSERT INTO messages
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [fromPublicKey, `, [fromPublicKey,
toPublicKey, toPublicKey,
content, content,
timestamp, timestamp,
0, //по умолчанию не прочитаны 0, //по умолчанию не прочитаны
'', '',
1, //Свои же сообщения всегда от нас 1, //Свои же сообщения всегда от нас
await encodeWithPassword(privatePlain, decryptedContent), await encodeWithPassword(privatePlain, decryptedContent),
publicKey, publicKey,
messageId, messageId,
DeliveredMessageState.DELIVERED, DeliveredMessageState.DELIVERED,
JSON.stringify(attachmentsMeta)]); JSON.stringify(attachmentsMeta)]);
updateDialog(toPublicKey); updateDialog(toPublicKey);
let dialogCache = getDialogCache(toPublicKey); let dialogCache = getDialogCache(toPublicKey);
if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) {
addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
} }
});
}, [privatePlain, currentDialogPublicKeyView]); }, [privatePlain, currentDialogPublicKeyView]);
/** /**
* Обработчик сообщений для группы * Обработчик сообщений для группы
*/ */
usePacket(0x06, async (packet: PacketMessage) => { usePacket(0x06, async (packet: PacketMessage) => {
const fromPublicKey = packet.getFromPublicKey(); runTaskInQueue(async () => {
const toPublicKey = packet.getToPublicKey(); const fromPublicKey = packet.getFromPublicKey();
const content = packet.getContent(); const toPublicKey = packet.getToPublicKey();
const timestamp = packet.getTimestamp(); const content = packet.getContent();
const messageId = packet.getMessageId(); const timestamp = packet.getTimestamp();
if(!hasGroup(toPublicKey)){ const messageId = packet.getMessageId();
/** if (!hasGroup(toPublicKey)) {
* Если это личное сообщение, то игнорируем его здесь
* для него есть отдельный слушатель usePacket (снизу)
*/
return;
}
if(fromPublicKey == publicKey){
/**
* Игнорируем свои же сообщения,
* такое получается при пакете синхронизации
*/
return;
}
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 и не нуждается * Если это личное сообщение, то игнорируем его здесь
* в последующем скачивании * для него есть отдельный слушатель usePacket (снизу)
*/ */
const decryptedBlob = await decodeWithPassword(groupKey, attachment.blob); return;
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, }
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); if (fromPublicKey == publicKey) {
messageAttachments[nextLength - 1].blob = decryptedBlob; /**
* Игнорируем свои же сообщения,
* такое получается при пакете синхронизации
*/
return;
}
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 = '';
} }
attachmentsMeta.push({ let attachmentsMeta: any[] = [];
id: attachment.id, let messageAttachments: Attachment[] = [];
type: attachment.type, for (let i = 0; i < packet.getAttachments().length; i++) {
preview: attachment.preview const attachment = packet.getAttachments()[i];
}); log("Attachment received id " + attachment.id + " type " + attachment.type);
}
const newMessage: Message = { let nextLength = messageAttachments.push({
from_public_key: fromPublicKey, ...attachment,
to_public_key: toPublicKey, blob: ""
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(` 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 INSERT INTO messages
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [fromPublicKey, `, [fromPublicKey,
toPublicKey, toPublicKey,
content, content,
timestamp, timestamp,
/**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */ /**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */
(currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, (currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0,
'', '',
0, 0,
await encodeWithPassword(privatePlain, decryptedContent), await encodeWithPassword(privatePlain, decryptedContent),
publicKey, publicKey,
messageId, messageId,
DeliveredMessageState.DELIVERED, DeliveredMessageState.DELIVERED,
JSON.stringify(attachmentsMeta)]); JSON.stringify(attachmentsMeta)]);
/**
* Так как у нас в toPublicKey приходит ID группы,
* то обновляем диалог по этому ID, а не по fromPublicKey
* как это сделано в личных сообщениях
*/
updateDialog(toPublicKey);
if (((normalize(currentDialogPublicKeyView) !== normalize(toPublicKey) || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) &&
(timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) {
/** /**
* Условие со временем нужно для того, * Так как у нас в toPublicKey приходит ID группы,
* чтобы когда приходит пачка сообщений с сервера в момент того как * то обновляем диалог по этому ID, а не по fromPublicKey
* пользователь был неактивен, не слать уведомления по всем этим сообщениям * как это сделано в личных сообщениях
*/ */
let mentionFlag = false; updateDialog(toPublicKey);
if((newMessage.from_public_key != publicKey) && (decryptedContent.includes(`@${userInfo.username}`) || decryptedContent.includes(`@all`))){
/**
* Если в сообщении есть упоминание текущего пользователя или @all,
* при этом сообщение отправляли не мы,
* то добавляем упоминание в состояние диалога.
*
* TODO: сделать чтобы all работал только для админов группы
*/
mentionFlag = true;
}
if(!muted.includes(toPublicKey) || mentionFlag){ if (((normalize(currentDialogPublicKeyView) !== normalize(toPublicKey) || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) &&
(timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) {
/** /**
* Если группа не в мутие или есть упоминание - отправляем уведомление * Условие со временем нужно для того,
* чтобы когда приходит пачка сообщений с сервера в момент того как
* пользователь был неактивен, не слать уведомления по всем этим сообщениям
*/ */
notify("New message", "You have a new message"); let mentionFlag = false;
if ((newMessage.from_public_key != publicKey) && (decryptedContent.includes(`@${userInfo.username}`) || decryptedContent.includes(`@all`))) {
/**
* Если в сообщении есть упоминание текущего пользователя или @all,
* при этом сообщение отправляли не мы,
* то добавляем упоминание в состояние диалога.
*
* TODO: сделать чтобы all работал только для админов группы
*/
mentionFlag = true;
}
if (!muted.includes(toPublicKey) || mentionFlag) {
/**
* Если группа не в мутие или есть упоминание - отправляем уведомление
*/
notify("New message", "You have a new message");
}
if (mentionFlag) {
/**
* Если в сообщении есть упоминание текущего пользователя или @all,
* то добавляем упоминание в состояние диалога
*
* TODO: сделать чтобы all работал только для админов группы
*/
pushMention({
dialog_id: toPublicKey,
message_id: messageId
});
}
} }
if(mentionFlag){ let dialogCache = getDialogCache(toPublicKey);
/** if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) {
* Если в сообщении есть упоминание текущего пользователя или @all, addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
* то добавляем упоминание в состояние диалога
*
* TODO: сделать чтобы all работал только для админов группы
*/
pushMention({
dialog_id: toPublicKey,
message_id: messageId
});
} }
} });
let dialogCache = getDialogCache(toPublicKey);
if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) {
addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
}
}, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]);
/** /**
* Обработчик личных сообщений * Обработчик личных сообщений
*/ */
usePacket(0x06, async (packet: PacketMessage) => { usePacket(0x06, async (packet: PacketMessage) => {
const fromPublicKey = packet.getFromPublicKey(); runTaskInQueue(async () => {
if(fromPublicKey == publicKey){ const fromPublicKey = packet.getFromPublicKey();
/** if (fromPublicKey == publicKey) {
* Игнорируем свои же сообщения,
* такое получается при пакете синхронизации
*/
return;
}
const toPublicKey = packet.getToPublicKey();
const content = packet.getContent();
const chachaKey = packet.getChachaKey();
const timestamp = packet.getTimestamp();
const messageId = packet.getMessageId();
if(hasGroup(toPublicKey)){
/**
* Если это групповое сообщение, то игнорируем его здесь
* для него есть отдельный слушатель usePacket
*/
return;
}
info("New message packet received from " + fromPublicKey);
if (blocked.includes(fromPublicKey)) {
/**
* Если пользователь заблокирован и это не групповое сообщение,
* то игнорируем сообщение
*/
log("Message from blocked user, ignore " + fromPublicKey);
return;
}
if (privatePlain == "") {
return;
}
const chachaDecryptedKey = Buffer.from(await decrypt(chachaKey, privatePlain), "binary");
const key = chachaDecryptedKey.slice(0, 32);
const nonce = chachaDecryptedKey.slice(32);
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
let 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); return;
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, }
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); const toPublicKey = packet.getToPublicKey();
messageAttachments[nextLength - 1].blob = decryptedBlob; const content = packet.getContent();
const chachaKey = packet.getChachaKey();
const timestamp = packet.getTimestamp();
const messageId = packet.getMessageId();
if (hasGroup(toPublicKey)) {
/**
* Если это групповое сообщение, то игнорируем его здесь
* для него есть отдельный слушатель usePacket
*/
return;
}
info("New message packet received from " + fromPublicKey);
if (blocked.includes(fromPublicKey)) {
/**
* Если пользователь заблокирован и это не групповое сообщение,
* то игнорируем сообщение
*/
log("Message from blocked user, ignore " + fromPublicKey);
return;
} }
attachmentsMeta.push({ if (privatePlain == "") {
id: attachment.id, return;
type: attachment.type, }
preview: attachment.preview
});
}
const newMessage: Message = { const chachaDecryptedKey = Buffer.from(await decrypt(chachaKey, privatePlain), "binary");
from_public_key: fromPublicKey, const key = chachaDecryptedKey.slice(0, 32);
to_public_key: toPublicKey, const nonce = chachaDecryptedKey.slice(32);
content: content, const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
timestamp: timestamp,
readed: idle ? 0 : 1, let attachmentsMeta: any[] = [];
chacha_key: chachaDecryptedKey.toString('utf-8'), let messageAttachments: Attachment[] = [];
from_me: fromPublicKey == publicKey ? 1 : 0, for (let i = 0; i < packet.getAttachments().length; i++) {
plain_message: (decryptedContent as string), const attachment = packet.getAttachments()[i];
delivered: DeliveredMessageState.DELIVERED, log("Attachment received id " + attachment.id + " type " + attachment.type);
message_id: messageId,
attachments: messageAttachments 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: idle ? 0 : 1,
chacha_key: chachaDecryptedKey.toString('utf-8'),
from_me: fromPublicKey == publicKey ? 1 : 0,
plain_message: (decryptedContent as string),
delivered: DeliveredMessageState.DELIVERED,
message_id: messageId,
attachments: messageAttachments
};
await runQuery(` await runQuery(`
INSERT INTO messages INSERT INTO messages
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [fromPublicKey, `, [fromPublicKey,
toPublicKey, toPublicKey,
content, content,
timestamp, timestamp,
(currentDialogPublicKeyView == fromPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, (currentDialogPublicKeyView == fromPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0,
chachaKey, chachaKey,
0, 0,
await encodeWithPassword(privatePlain, decryptedContent), await encodeWithPassword(privatePlain, decryptedContent),
publicKey, publicKey,
messageId, messageId,
DeliveredMessageState.DELIVERED, DeliveredMessageState.DELIVERED,
JSON.stringify(attachmentsMeta)]); JSON.stringify(attachmentsMeta)]);
log("New message received from " + fromPublicKey); log("New message received from " + fromPublicKey);
updateDialog(fromPublicKey); updateDialog(fromPublicKey);
if (((currentDialogPublicKeyView !== fromPublicKey || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) && if (((currentDialogPublicKeyView !== fromPublicKey || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) &&
(timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) { (timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) {
/**
* Условие со временем нужно для того,
* чтобы когда приходит пачка сообщений с сервера в момент того как
* пользователь был неактивен, не слать уведомления по всем этим сообщениям
*/
if(!muted.includes(fromPublicKey)){
/** /**
* Если пользователь в муте - не отправляем уведомление * Условие со временем нужно для того,
* чтобы когда приходит пачка сообщений с сервера в момент того как
* пользователь был неактивен, не слать уведомления по всем этим сообщениям
*/ */
notify("New message", "You have a new message"); if (!muted.includes(fromPublicKey)) {
/**
* Если пользователь в муте - не отправляем уведомление
*/
notify("New message", "You have a new message");
}
} }
} let dialogCache = getDialogCache(fromPublicKey);
let dialogCache = getDialogCache(fromPublicKey); if (currentDialogPublicKeyView !== fromPublicKey && dialogCache.length > 0) {
if (currentDialogPublicKeyView !== fromPublicKey && dialogCache.length > 0) { addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); }
} });
}, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]);
/** /**
* Обработчик синхронизации прочтения личных сообщений * Обработчик синхронизации прочтения личных сообщений
*/ */
usePacket(0x07, async (packet: PacketRead) => { usePacket(0x07, async (packet: PacketRead) => {
if(hasGroup(packet.getToPublicKey())){ runTaskInQueue(async () => {
/** if (hasGroup(packet.getToPublicKey())) {
* Если это относится к группам, то игнорируем здесь, /**
* для этого есть отдельный слушатель usePacket ниже * Если это относится к группам, то игнорируем здесь,
*/ * для этого есть отдельный слушатель usePacket ниже
return; */
} 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; 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]); }, [updateDialog, publicKey]);
/** /**
* Обработчик прочтения личных сообщений * Обработчик прочтения личных сообщений
*/ */
usePacket(0x07, async (packet: PacketRead) => { usePacket(0x07, async (packet: PacketRead) => {
if(hasGroup(packet.getToPublicKey())){ runTaskInQueue(async () => {
/** if (hasGroup(packet.getToPublicKey())) {
* Если это относится к группам, то игнорируем здесь, /**
* для этого есть отдельный слушатель usePacket ниже * Если это относится к группам, то игнорируем здесь,
*/ * для этого есть отдельный слушатель usePacket ниже
return; */
} return;
const fromPublicKey = packet.getFromPublicKey();
const toPublicKey = packet.getToPublicKey();
if(fromPublicKey == publicKey){
/**
* Игнорируем если это наше прочтение
* которое получается при синхронизации
*/
return;
}
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]);
updateDialog(fromPublicKey);
log("Read packet received from " + fromPublicKey + " for " + 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; const fromPublicKey = packet.getFromPublicKey();
})); const toPublicKey = packet.getToPublicKey();
if (fromPublicKey == publicKey) {
/**
* Игнорируем если это наше прочтение
* которое получается при синхронизации
*/
return;
}
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) {
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
console.info({ fromPublicKey, toPublicKey });
return {
...message,
readed: 1
}
}
return message;
}));
});
}, [updateDialog, publicKey]); }, [updateDialog, publicKey]);
/** /**
* Обработчик прочтения групповых сообщений * Обработчик прочтения групповых сообщений
*/ */
usePacket(0x07, async (packet: PacketRead) => { usePacket(0x07, async (packet: PacketRead) => {
if(!hasGroup(packet.getToPublicKey())){ runTaskInQueue(async () => {
/** if (!hasGroup(packet.getToPublicKey())) {
* Если это не относится к группам, то игнорируем здесь, /**
* для этого есть отдельный слушатель usePacket выше * Если это не относится к группам, то игнорируем здесь,
*/ * для этого есть отдельный слушатель usePacket выше
return; */
} return;
const fromPublicKey = packet.getFromPublicKey();
const toPublicKey = packet.getToPublicKey();
await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]);
updateDialog(toPublicKey);
addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => {
if (!message.readed) {
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
console.info({fromPublicKey, toPublicKey});
return {
...message,
readed: 1
}
} }
return message; const fromPublicKey = packet.getFromPublicKey();
})); const toPublicKey = packet.getToPublicKey();
await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]);
updateDialog(toPublicKey);
addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => {
if (!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]); }, [updateDialog]);
/** /**
* Обработчик доставки сообщений * Обработчик доставки сообщений
*/ */
usePacket(0x08, async (packet: PacketDelivery) => { usePacket(0x08, async (packet: PacketDelivery) => {
const messageId = packet.getMessageId(); runTaskInQueue(async () => {
await runQuery(`UPDATE messages SET delivered = ?, timestamp = ? WHERE message_id = ? AND account = ?`, [DeliveredMessageState.DELIVERED, Date.now(), messageId, publicKey]); const messageId = packet.getMessageId();
updateDialog(packet.getToPublicKey()); await runQuery(`UPDATE messages SET delivered = ?, timestamp = ? WHERE message_id = ? AND account = ?`, [DeliveredMessageState.DELIVERED, Date.now(), messageId, publicKey]);
log("Delivery packet received msg id " + messageId); updateDialog(packet.getToPublicKey());
addOrUpdateDialogCache(packet.getToPublicKey(), getDialogCache(packet.getToPublicKey()).map((message) => { log("Delivery packet received msg id " + messageId);
if (message.message_id == messageId) { addOrUpdateDialogCache(packet.getToPublicKey(), getDialogCache(packet.getToPublicKey()).map((message) => {
return { if (message.message_id == messageId) {
...message, return {
delivered: DeliveredMessageState.DELIVERED, ...message,
timestamp: Date.now() delivered: DeliveredMessageState.DELIVERED,
timestamp: Date.now()
}
} }
} return message;
return message; }));
})); });
}, [updateDialog]); }, [updateDialog]);
} }

View File

@@ -0,0 +1,57 @@
import { useEffect } from "react";
import { useProtocolState } from "../ProtocolProvider/useProtocolState";
import { ProtocolState } from "../ProtocolProvider/ProtocolProvider";
import { useDatabase } from "../DatabaseProvider/useDatabase";
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";
/**
* Хук отвечает за синхронизацию сообщений, запрос синхронизации
* при подключении
*/
export function useSynchronize() {
const [protocolState, setProtocolState] = useProtocolState();
const {getQuery} = useDatabase();
const publicKey = usePublicKey();
const send = useSender();
useEffect(() => {
if(protocolState == ProtocolState.CONNECTED){
trySync();
setProtocolState(ProtocolState.SYNCHRONIZATION);
}
}, [protocolState]);
const trySync = async () => {
const lastMessage = await getQuery("SELECT timestamp FROM messages WHERE account = ? ORDER BY timestamp DESC LIMIT 1", [publicKey]);
if(!lastMessage){
sendSynchronize(0);
return;
}
sendSynchronize(lastMessage.timestamp);
}
const sendSynchronize = (timestamp: number) => {
const packet = new PacketSync();
packet.setStatus(0);
packet.setTimestamp(timestamp);
send(packet);
}
usePacket(25, async (packet: PacketSync) => {
const status = packet.getStatus();
if(status == SyncStatus.BATCH_END){
await whenFinish();
trySync();
}
if(status == SyncStatus.NOT_NEEDED){
/**
* Синхронизация не нужна, все данные актуальны
*/
setProtocolState(ProtocolState.CONNECTED);
}
});
}

View File

@@ -12,10 +12,13 @@ export enum ProtocolState {
HANDSHAKE_EXCHANGE, HANDSHAKE_EXCHANGE,
DISCONNECTED, DISCONNECTED,
RECONNECTING, RECONNECTING,
DEVICE_VERIFICATION_REQUIRED DEVICE_VERIFICATION_REQUIRED,
SYNCHRONIZATION
} }
export const ProtocolContext = createContext<[Protocol|null, ProtocolState]>([null, ProtocolState.DISCONNECTED]); export type ProtocolContextType = [Protocol|null, ProtocolState, (state: ProtocolState) => void];
export const ProtocolContext = createContext<ProtocolContextType>([null, ProtocolState.DISCONNECTED, () => {}]);
interface ProtocolProviderProps { interface ProtocolProviderProps {
children: React.ReactNode; children: React.ReactNode;
@@ -91,7 +94,7 @@ export function ProtocolProvider(props : ProtocolProviderProps) {
}, [publicKey, privateKey, systemInfo.id]); }, [publicKey, privateKey, systemInfo.id]);
return ( return (
<ProtocolContext.Provider value={[protocol, connect]}> <ProtocolContext.Provider value={[protocol, connect, setConnect]}>
{props.children} {props.children}
</ProtocolContext.Provider> </ProtocolContext.Provider>
); );

View File

@@ -0,0 +1,48 @@
import Packet from "../packet";
import Stream from "../stream";
export enum SyncStatus {
NOT_NEEDED,
BATCH_START,
BATCH_END
}
export class PacketSync extends Packet {
private status : SyncStatus = SyncStatus.NOT_NEEDED;
private timestamp : number = 0;
public getPacketId(): number {
return 25; //0x19
}
public _receive(stream: Stream): void {
this.status = stream.readInt8() as SyncStatus;
this.timestamp = stream.readInt64();
}
public _send(): Promise<Stream> | Stream {
let stream = new Stream();
stream.writeInt16(this.getPacketId());
stream.writeInt8(this.status);
stream.writeInt64(this.timestamp);
return stream;
}
public getStatus() : SyncStatus {
return this.status;
}
public setStatus(status: SyncStatus) {
this.status = status;
}
public getTimestamp() : number {
return this.timestamp;
}
public setTimestamp(timestamp: number) {
this.timestamp = timestamp;
}
}

View File

@@ -24,6 +24,7 @@ import { PacketGroupBan } from "./packets/packet.group.ban";
import { PacketDeviceNew } from "./packets/packet.device.new"; import { PacketDeviceNew } from "./packets/packet.device.new";
import { PacketDeviceList } from "./packets/packet.device.list"; import { PacketDeviceList } from "./packets/packet.device.list";
import { PacketDeviceResolve } from "./packets/packet.device.resolve"; import { PacketDeviceResolve } from "./packets/packet.device.resolve";
import { PacketSync } from "./packets/packet.sync";
export default class Protocol extends EventEmitter { export default class Protocol extends EventEmitter {
private serverAddress: string; private serverAddress: string;
@@ -123,6 +124,7 @@ export default class Protocol extends EventEmitter {
this._supportedPackets.set(0x16, new PacketGroupBan()); this._supportedPackets.set(0x16, new PacketGroupBan());
this._supportedPackets.set(0x17, new PacketDeviceList()); this._supportedPackets.set(0x17, new PacketDeviceList());
this._supportedPackets.set(0x18, new PacketDeviceResolve()); this._supportedPackets.set(0x18, new PacketDeviceResolve());
this._supportedPackets.set(25, new PacketSync());
} }
private _findWaiters(packetId: number): ((packet: Packet) => void)[] { private _findWaiters(packetId: number): ((packet: Packet) => void)[] {

View File

@@ -1,12 +1,12 @@
import { useContext } from "react"; import { useContext } from "react";
import { ProtocolContext } from "./ProtocolProvider"; import { ProtocolContext, ProtocolContextType, ProtocolState } from "./ProtocolProvider";
export const useProtocolState = () => { export const useProtocolState = () => {
const [context, connect] = useContext(ProtocolContext); const context : ProtocolContextType = useContext(ProtocolContext);
if(!context){ if(!context){
throw new Error("useProtocol must be used within a ProtocolProvider"); throw new Error("useProtocol must be used within a ProtocolProvider");
} }
return connect; return [context[1], context[2]] as [ProtocolState, (state: ProtocolState) => void];
}; };

View File

@@ -12,7 +12,7 @@ import { usePacket } from "@/app/providers/ProtocolProvider/usePacket";
import { PacketDeviceResolve, Solution } from "@/app/providers/ProtocolProvider/protocol/packets/packet.device.resolve"; import { PacketDeviceResolve, Solution } from "@/app/providers/ProtocolProvider/protocol/packets/packet.device.resolve";
export function DeviceConfirm() { export function DeviceConfirm() {
const protocolState = useProtocolState(); const [protocolState] = useProtocolState();
const navigate = useNavigate(); const navigate = useNavigate();
const logout = useLogout(); const logout = useLogout();

View File

@@ -30,6 +30,7 @@ import { useLogout } from "@/app/providers/AccountProvider/useLogout";
import { useUpdateMessage } from "@/app/hooks/useUpdateMessage"; import { useUpdateMessage } from "@/app/hooks/useUpdateMessage";
import { useDeviceMessage } from "@/app/hooks/useDeviceMessage"; import { useDeviceMessage } from "@/app/hooks/useDeviceMessage";
import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider"; import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider";
import { useSynchronize } from "@/app/providers/DialogProvider/useSynchronize";
export function Main() { export function Main() {
const { mainColor, borderColor } = useRosettaColors(); const { mainColor, borderColor } = useRosettaColors();
@@ -54,6 +55,11 @@ export function Main() {
*/ */
useDeviceMessage(); useDeviceMessage();
/**
* Синхронизируем сообщения при подключении
*/
useSynchronize();
const { setSize, setResizeble } = useWindow(); const { setSize, setResizeble } = useWindow();
/** /**