diff --git a/app/components/TextChain/TextChain.tsx b/app/components/TextChain/TextChain.tsx index 55a9e72..a63b3aa 100644 --- a/app/components/TextChain/TextChain.tsx +++ b/app/components/TextChain/TextChain.tsx @@ -5,6 +5,7 @@ import { useState, useEffect } from "react"; interface TextChainProps { text: string; mt?: MantineSize; + withoutPaper?: boolean; } export function TextChain(props : TextChainProps) { @@ -26,31 +27,41 @@ export function TextChain(props : TextChainProps) { }); }, [text]); + const grid = ( + + {text.split(" ").map((v, i) => { + return ( + + {i + 1}. + {v} + + ); + })} + + ); + return ( + !props.withoutPaper ? ( Your seed phrase: - - {text.split(" ").map((v, i) => { - return ( - - {i + 1}. - {v} - - ); - })} - + {grid} + ) : ( + + {grid} + + ) ) } \ No newline at end of file diff --git a/app/components/TextParser/TextParser.tsx b/app/components/TextParser/TextParser.tsx index 0573144..9cbdf4c 100644 --- a/app/components/TextParser/TextParser.tsx +++ b/app/components/TextParser/TextParser.tsx @@ -46,10 +46,11 @@ export function TextParser(props: TextParserProps) { { pattern: [ /(https?:\/\/[^\s]+)/g, - /\b(?:https?:\/\/)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/g + /\b(?:https?:\/\/)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[\w\-\.\/\?&#=%]*)?/g ], render: (match: string) => { - let domainZone = match.split('.').pop() || ''; + let domain = match.replace(/https?:\/\//, '').split('/')[0]; + let domainZone = domain.split('.').pop() || ''; domainZone = domainZone.split('/')[0]; if(!ALLOWED_DOMAINS_ZONES.includes(domainZone)) { return <>{match}; diff --git a/app/hooks/useFileStorage.ts b/app/hooks/useFileStorage.ts index 762addc..c29703e 100644 --- a/app/hooks/useFileStorage.ts +++ b/app/hooks/useFileStorage.ts @@ -9,5 +9,15 @@ export function useFileStorage() { return result; } - return {writeFile, readFile}; + const fileExists = async (file : string, inWorkingDir : boolean = true) => { + const result = await window.electron.ipcRenderer.invoke('fileStorage:fileExists', file, inWorkingDir); + return result; + } + + const size = async (file : string, inWorkingDir : boolean = true) => { + const result = await window.electron.ipcRenderer.invoke('fileStorage:size', file, inWorkingDir); + return result; + } + + return {writeFile, readFile, fileExists, size}; } \ No newline at end of file diff --git a/app/providers/AttachmentProvider/useAttachment.ts b/app/providers/AttachmentProvider/useAttachment.ts index fe25a0a..f7768f2 100644 --- a/app/providers/AttachmentProvider/useAttachment.ts +++ b/app/providers/AttachmentProvider/useAttachment.ts @@ -30,7 +30,7 @@ export function useAttachment(attachment: Attachment, keyPlain: string) { const downloadPercentage = useDownloadStatus(attachment.id); const [downloadStatus, setDownloadStatus] = useMemory("attachment-downloaded-status-" + attachment.id, DownloadStatus.PENDING, true); const [downloadTag, setDownloadTag] = useState(""); - const {readFile, writeFile} = useFileStorage(); + const {readFile, writeFile, fileExists, size} = useFileStorage(); const { downloadFile } = useTransport(); const publicKey = usePublicKey(); const privatePlain = usePrivatePlain(); @@ -83,10 +83,12 @@ export function useAttachment(attachment: Attachment, keyPlain: string) { * а в загрузках */ const preview = getPreview(); + const filesize = parseInt(preview.split("::")[0]); const filename = preview.split("::")[1]; let pathInDownloads = window.downloadsPath + "/Rosetta Downloads/" + filename; - const fileData = await readFile(pathInDownloads, false); - if(fileData){ + const exists = await fileExists(pathInDownloads, false); + const existsLength = await size(pathInDownloads, false); + if(exists && existsLength == filesize){ setDownloadStatus(DownloadStatus.DOWNLOADED); return; } @@ -161,7 +163,17 @@ export function useAttachment(attachment: Attachment, keyPlain: string) { const filename = preview.split("::")[1]; let buffer = Buffer.from(decrypted.split(",")[1], 'base64'); let pathInDownloads = window.downloadsPath + "/Rosetta Downloads/" + filename; - await writeFile(pathInDownloads, buffer, false); + /** + * Пишем файл в загрузки, но перед этим выбираем ему название, если файл в загрузках + * уже есть с таким названием то добавляем к названию (1), (2) и так далее, чтобы не перезаписать существующий файл + */ + let finalPath = pathInDownloads; + let fileIndex = 1; + while (await fileExists(finalPath, false)) { + finalPath = window.downloadsPath + "/Rosetta Downloads/" + filename.split(".").slice(0, -1).join(".") + ` (${fileIndex})` + "." + filename.split(".").slice(-1); + fileIndex++; + } + await writeFile(finalPath, buffer, false); setDownloadStatus(DownloadStatus.DOWNLOADED); return; } diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 43f244e..448edf4 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -105,6 +105,11 @@ export function DialogProvider(props: DialogProviderProps) { useEffect(() => { if(props.dialog == "demo"){ + /** + * Это нужно для демонстрации работы сообщений на странице оформления. Так как там нет хуков и + * других инструментов для загрузки сообщений, то мы просто не загружаем сообщения, так как это режим + * демонстрации + */ return; } if(idle){ @@ -122,6 +127,11 @@ export function DialogProvider(props: DialogProviderProps) { useEffect(() => { if(props.dialog == "demo"){ + /** + * Это нужно для демонстрации работы сообщений на странице оформления. Так как там нет хуков и + * других инструментов для загрузки сообщений, то мы просто не загружаем сообщения, так как это режим + * демонстрации + */ return; } setMessages([]); @@ -184,15 +194,13 @@ export function DialogProvider(props: DialogProviderProps) { readUpdated = true; } let decryptKey = ''; - if(message.from_me){ + if(message.from_me && message.chacha_key != "" && message.chacha_key.startsWith("sync:")){ /** - * Если сообщение от меня, то ключ расшифровки для вложений - * не нужен, передаем пустую строку, так как под капотом - * в MessageAttachment.tsx при расшифровке вложений используется - * локальный ключ, а не тот что в сообщении, так как файл и так находится - * у нас локально + * Если это сообщение от нас, то проверяем, есть ли внутри chacha_key, если есть, значит это + * сообщение пришло нам в результате синхронизации и его нужно расшифровать, если chacha_key нет, + * значит сообщение отправлено с нашего устройства, и зашифровано на стороне отправки (plain_message) */ - decryptKey = ''; + decryptKey = Buffer.from(await decodeWithPassword(privatePlain, message.chacha_key.replace("sync:", "")), 'binary').toString('utf-8'); } if(hasGroup(props.dialog)){ /** @@ -418,6 +426,12 @@ export function DialogProvider(props: DialogProviderProps) { */ return; } + if(toPublicKey != props.dialog) { + /** + * Игнорируем если это не сообщение для этого диалога + */ + return; + } const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary"); const key = chachaDecryptedKey.slice(0, 32); const nonce = chachaDecryptedKey.slice(32); @@ -458,6 +472,13 @@ export function DialogProvider(props: DialogProviderProps) { const fromPublicKey = packet.getFromPublicKey(); const toPublicKey = packet.getToPublicKey(); + if(fromPublicKey == publicKey){ + /** + * Это синхронизация, игнорируем ее в этом обработчике + */ + return; + } + if(hasGroup(props.dialog)){ /** * Если это групповое сообщение, то для него есть @@ -529,6 +550,13 @@ export function DialogProvider(props: DialogProviderProps) { const fromPublicKey = packet.getFromPublicKey(); const toPublicKey = packet.getToPublicKey(); + if(fromPublicKey == publicKey){ + /** + * Это синхронизация, игнорируем ее в этом обработчике + */ + return; + } + if(toPublicKey != props.dialog){ /** * Исправление кросс диалогового сообщения diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index 4bb598f..0831a27 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -96,15 +96,15 @@ export function useDialogFiber() { const content = packet.getContent(); const timestamp = packet.getTimestamp(); const messageId = packet.getMessageId(); - - if (fromPublicKey != publicKey) { /** * Игнорируем если это не сообщение от нас */ return; } - const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary"); + + 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')); @@ -160,7 +160,7 @@ export function useDialogFiber() { content, timestamp, 0, //по умолчанию не прочитаны - '', + "sync:" + aesChachaKey, 1, //Свои же сообщения всегда от нас await encodeWithPassword(privatePlain, decryptedContent), publicKey, diff --git a/app/version.ts b/app/version.ts index 169916b..95ebf02 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,7 +1,11 @@ -export const APP_VERSION = "1.0.1"; -export const CORE_MIN_REQUIRED_VERSION = "1.4.8"; +export const APP_VERSION = "1.0.2"; +export const CORE_MIN_REQUIRED_VERSION = "1.4.9"; export const RELEASE_NOTICE = ` -**Update v1.0.1** :emoji_1f631: -- Fix push notifications on synchronization +**Update v1.0.2** :emoji_1f631: +- Support multiple file downloads +- Fix fallback after boot loading +- Fix corss-chat reading messages +- Support sync attachments on other devices +- Fix UI bugs `; \ No newline at end of file diff --git a/app/views/Backup/Backup.tsx b/app/views/Backup/Backup.tsx index 29a30cf..411aadf 100644 --- a/app/views/Backup/Backup.tsx +++ b/app/views/Backup/Backup.tsx @@ -5,7 +5,7 @@ import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput"; import { TextChain } from "@/app/components/TextChain/TextChain"; import { decodeWithPassword } from "@/app/crypto/crypto"; import { useAccount } from "@/app/providers/AccountProvider/useAccount"; -import { Paper, Text } from "@mantine/core"; +import { Text } from "@mantine/core"; import { useState } from "react"; export function Backup() { @@ -39,10 +39,8 @@ export function Backup() { {show.trim() !== "" && ( <> - - - - + + Please don't share your seed phrase! The administration will never ask you for it. diff --git a/lib/main/boot/bootloader.ts b/lib/main/boot/bootloader.ts index 531db5b..638d3c5 100644 --- a/lib/main/boot/bootloader.ts +++ b/lib/main/boot/bootloader.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain } from "electron"; +import { app, BrowserWindow } from "electron"; import fs from 'fs/promises' import { WORKING_DIR } from "../constants"; import path from "path"; @@ -6,30 +6,6 @@ import { Logger } from "../logger"; const logger = Logger('bootloader'); - -ipcMain.handleOnce('report-boot-process-failed', async () => { - /** - * Если процесс загрузки не завершился успешно, то preload показывает - * экран ошибки, а нам нужно откатиться назад к загрузке dev.html - * и удалить скомпилированные файлы, чтобы при следующем запуске - * приложение попыталось загрузиться в режиме разработки. - */ - let filePath = path.join(WORKING_DIR, 'b'); - if(!await existsFile(filePath)){ - /** - * Исправление ошибки когда директории нет. - */ - logger.log("No compiled files to remove"); - return; - } - await fs.rmdir(filePath, { recursive: true }); - logger.log("Boot process failed, removed compiled files"); - logger.log(`Removed compiled files at ${filePath}`); - logger.log(`Restarting application in safe mode`); - app.relaunch(); - app.exit(0); -}); - /** * Boot функция, эта функция запускает приложение * @param window окно diff --git a/lib/main/ipcs/ipcFilestorage.ts b/lib/main/ipcs/ipcFilestorage.ts index 71c8abd..111049d 100644 --- a/lib/main/ipcs/ipcFilestorage.ts +++ b/lib/main/ipcs/ipcFilestorage.ts @@ -2,16 +2,20 @@ import { ipcMain } from "electron"; import { WORKING_DIR } from "../constants"; import fs from 'fs/promises' import path from 'path' +import { Logger } from "../logger"; + +const logger = Logger('ipcFilestorage'); ipcMain.handle('fileStorage:writeFile', async (_, file: string, data: string | Buffer, inWorkingDir : boolean = true) => { + logger.log(`System call fileStorage:writeFile with file=${file} inWorkingDir=${inWorkingDir}`); const fullPath = path.join(inWorkingDir ? WORKING_DIR : '', file); await fs.mkdir(path.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, data); - console.info("File written to " + fullPath); return true; }); ipcMain.handle('fileStorage:readFile', async (_, file: string, inWorkingDir : boolean = true) => { + logger.log(`System call fileStorage:readFile with file=${file} inWorkingDir=${inWorkingDir}`); try{ const fullPath = path.join(inWorkingDir ? WORKING_DIR : '', file); const data = await fs.readFile(fullPath); @@ -19,4 +23,26 @@ ipcMain.handle('fileStorage:readFile', async (_, file: string, inWorkingDir : bo }catch(e){ return null; } +}); + +ipcMain.handle('fileStorage:size', async (_, file: string, inWorkingDir : boolean = true) => { + logger.log(`System call fileStorage:size with file=${file} inWorkingDir=${inWorkingDir}`); + try{ + const fullPath = path.join(inWorkingDir ? WORKING_DIR : '', file); + const stats = await fs.stat(fullPath); + return stats.size; + }catch(e){ + return 0; + } +}); + +ipcMain.handle('fileStorage:fileExists', async (_, file: string, inWorkingDir : boolean = true) => { + logger.log(`System call fileStorage:fileExists with file=${file} inWorkingDir=${inWorkingDir}`); + try{ + const fullPath = path.join(inWorkingDir ? WORKING_DIR : '', file); + await fs.access(fullPath); + return true; + }catch(e){ + return false; + } }); \ No newline at end of file diff --git a/lib/preload/preload.ts b/lib/preload/preload.ts index 0fa6029..6135ec5 100644 --- a/lib/preload/preload.ts +++ b/lib/preload/preload.ts @@ -2,86 +2,6 @@ import { contextBridge, ipcRenderer, shell } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import api from './api' -const applicationLoader = ` -
-
- -
-`; - -const applicationError = ` -
-
-
- -
-

Application Error

-

The application failed to load properly. Please wait for application repairing or reinstall application.

- ${applicationLoader} -
-
-

rosetta - powering freedom. visit about rosetta-im.com. error: boot_process_failed

-
-
-`; - const exposeContext = async () => { let version = await ipcRenderer.invoke("get-core-version"); @@ -89,35 +9,6 @@ const exposeContext = async () => { let arch = await ipcRenderer.invoke("get-arch"); let deviceName = await ipcRenderer.invoke("device:name"); let deviceId = await ipcRenderer.invoke("device:id"); - let interval : any = 0; - - interval = setInterval(() => { - /** - * Если после определенного таймаута приложение так и - * не загрузилось, то считаем, что процесс завис, - * и показываем экран ошибки. Так же отправляем - * сигнал в main процесс, чтобы тот мог попытаться - * откатить обновление - */ - if (document.body.innerHTML.indexOf("preloadersignature") !== -1) { - /** - * Если сейчас показывается прелоадер, то не считаем - * что обновление битое, так как само обновление еще не - * загрузилось в приложение - */ - return; - } - if (document.body.innerHTML.length < 100) { - /** - * Приложение загружено, а прошло больше 5 секунд - * с момента прелоадера, значит что-то пошло не так - * и нужно показать экран ошибки - */ - document.body.innerHTML = applicationError; - ipcRenderer.invoke("report-boot-process-failed"); - } - clearInterval(interval); - }, 5000); let downloadsPath = await ipcRenderer.invoke("get-downloads-path"); if (process.contextIsolated) {