From 8d6090e6328209b15f3bb1c06b3cd22a21b3fd31 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Mar 2026 22:29:03 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=81=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BC=D0=B0=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/DialogInput/DialogInput.tsx | 25 ++- .../MessageAttachments/MessageImage.tsx | 1 + .../AttachmentProvider/useAttachment.ts | 29 +-- .../usePrepareAttachment.ts | 29 +-- app/providers/CallProvider/CallProvider.tsx | 3 + .../DialogProvider/DialogProvider.tsx | 24 ++- app/providers/DialogProvider/useDialog.ts | 5 +- .../DialogProvider/useSynchronize.ts | 16 +- .../TransportProvider/TransportProvider.tsx | 8 +- .../TransportProvider/useTransportServer.ts | 12 ++ app/servers.ts | 4 +- lib/main/main.ts | 181 ++++++++++-------- 12 files changed, 192 insertions(+), 145 deletions(-) create mode 100644 app/providers/TransportProvider/useTransportServer.ts diff --git a/app/components/DialogInput/DialogInput.tsx b/app/components/DialogInput/DialogInput.tsx index 4cb6354..7814b9d 100644 --- a/app/components/DialogInput/DialogInput.tsx +++ b/app/components/DialogInput/DialogInput.tsx @@ -89,7 +89,10 @@ export function DialogInput() { blob: fileContent, id: generateRandomKey(8), type: AttachmentType.FILE, - preview: files[0].size + "::" + files[0].name + preview: files[0].size + "::" + files[0].name, + transport_server: "", + transport_tag: "", + encoded_for: dialog }]); } }); @@ -116,7 +119,10 @@ export function DialogInput() { type: AttachmentType.MESSAGES, id: generateRandomKey(8), blob: JSON.stringify([...replyMessages.messages]), - preview: "" + preview: "", + transport_server: "", + transport_tag: "", + encoded_for: dialog }]); if(editableDivRef.current){ editableDivRef.current.focus(); @@ -230,7 +236,10 @@ export function DialogInput() { blob: avatars[0].avatar, id: generateRandomKey(8), type: AttachmentType.AVATAR, - preview: "" + preview: "", + transport_server: "", + transport_tag: "", + encoded_for: dialog }]); if(editableDivRef.current){ editableDivRef.current.focus(); @@ -270,7 +279,10 @@ export function DialogInput() { blob: base64Image, id: attachmentId, type: AttachmentType.IMAGE, - preview: "" + preview: "", + transport_server: "", + transport_tag: "", + encoded_for: dialog }]); } if(editableDivRef.current){ @@ -304,7 +316,10 @@ export function DialogInput() { blob: fileContent, id: attachmentId, type: AttachmentType.FILE, - preview: files[0].size + "::" + files[0].name + preview: files[0].size + "::" + files[0].name, + transport_server: "", + transport_tag: "", + encoded_for: dialog }]); } diff --git a/app/components/MessageAttachments/MessageImage.tsx b/app/components/MessageAttachments/MessageImage.tsx index 3156eba..a035d06 100644 --- a/app/components/MessageAttachments/MessageImage.tsx +++ b/app/components/MessageAttachments/MessageImage.tsx @@ -29,6 +29,7 @@ export function MessageImage(props: AttachmentProps) { const [blurhashPreview, setBlurhashPreview] = useState(""); useEffect(() => { + console.info(props.attachment); console.info("Consturcting image, download status: " + downloadStatus); constructBlob(); constructFromBlurhash(); diff --git a/app/providers/AttachmentProvider/useAttachment.ts b/app/providers/AttachmentProvider/useAttachment.ts index 8190277..d336eec 100644 --- a/app/providers/AttachmentProvider/useAttachment.ts +++ b/app/providers/AttachmentProvider/useAttachment.ts @@ -27,11 +27,10 @@ export enum DownloadStatus { } export function useAttachment(attachment: Attachment, parentMessage: MessageProps) { - const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; const uploadedPercentage = useUploadStatus(attachment.id); const downloadPercentage = useDownloadStatus(attachment.id); const [downloadStatus, setDownloadStatus] = useMemory("attachment-downloaded-status-" + attachment.id, DownloadStatus.PENDING, true); - const [downloadTag, setDownloadTag] = useState(""); + const [downloadTag, setDownloadTag] = useState(attachment.transport_tag || ""); const {readFile, writeFile, fileExists, size} = useFileStorage(); const { downloadFile } = useTransport(); const publicKey = usePublicKey(); @@ -50,30 +49,18 @@ export function useAttachment(attachment: Attachment, parentMessage: MessageProp }, []); const getPreview = () => { - if(attachment.preview.split("::")[0].match(uuidRegex)){ - /** - * Это тег загрузки - */ - return attachment.preview.split("::").splice(1).join("::"); - } return attachment.preview; } const calcDownloadStatus = async () => { - if(attachment.preview.split("::")[0].match(uuidRegex)){ - /** - * Это тег загрузки - */ - setDownloadTag(attachment.preview.split("::")[0]); - } - if(!attachment.preview.split("::")[0].match(uuidRegex)){ - /** - * Там не тег загрузки, значит это наш файл - */ - setDownloadStatus(DownloadStatus.DOWNLOADED); + if (downloadStatus == DownloadStatus.DOWNLOADED) { return; } - if (downloadStatus == DownloadStatus.DOWNLOADED) { + if(attachment.transport_tag == ""){ + /** + * Транспортного тега нет только у сообщений отправленных нами, значит он точно наш + */ + setDownloadStatus(DownloadStatus.DOWNLOADED); return; } if(attachment.type == AttachmentType.FILE){ @@ -143,7 +130,7 @@ export function useAttachment(attachment: Attachment, parentMessage: MessageProp let downloadedBlob = ''; try { downloadedBlob = await downloadFile(attachment.id, - downloadTag); + downloadTag, attachment.transport_server); } catch (e) { console.info(e); info("Error downloading attachment: " + attachment.id); diff --git a/app/providers/AttachmentProvider/usePrepareAttachment.ts b/app/providers/AttachmentProvider/usePrepareAttachment.ts index 71b6623..466b1df 100644 --- a/app/providers/AttachmentProvider/usePrepareAttachment.ts +++ b/app/providers/AttachmentProvider/usePrepareAttachment.ts @@ -10,6 +10,7 @@ import { useDatabase } from "../DatabaseProvider/useDatabase"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; import { useDialogsCache } from "../DialogProvider/useDialogsCache"; import { DialogContext } from "../DialogProvider/DialogProvider"; +import { useTransportServer } from "../TransportProvider/useTransportServer"; export function usePrepareAttachment() { const intervalsRef = useRef(null); @@ -19,6 +20,7 @@ export function usePrepareAttachment() { const {info} = useConsoleLogger('usePrepareAttachment'); const {getDialogCache} = useDialogsCache(); const context = useContext(DialogContext); + const transportServer = useTransportServer(); const updateTimestampInDialogCache = (dialog : string, message_id: string) => { const dialogCache = getDialogCache(dialog); @@ -74,18 +76,6 @@ export function usePrepareAttachment() { }, (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("::"); - } - /** * Подготавливает вложения для отправки. Подготовка * состоит в загрузке файлов на транспортный сервер, мы не делаем @@ -127,6 +117,16 @@ export function usePrepareAttachment() { const blurhash = await base64ImageToBlurhash(attachment.blob); attachment.preview = blurhash; } + if(rePrepared && attachment.encoded_for == dialog){ + /** + * Это пересланное сообщение и оно уже закодировано для этого диалога, значит не нужно его кодировать и загружать заново + */ + prepared.push({ + ...attachment, + blob: "" + }); + continue; + } doTimestampUpdateImMessageWhileAttachmentsSend(message_id, dialog); const content = await encodeWithPassword(password, attachment.blob); const upid = attachment.id; @@ -139,7 +139,10 @@ export function usePrepareAttachment() { } prepared.push({ ...attachment, - preview: tag + "::" + (rePrepared ? removeOldTagIfAttachemtnsRePreapred(attachment.preview) : attachment.preview), + transport_server: transportServer || "", + transport_tag: tag, + encoded_for: dialog, + preview: attachment.preview, blob: "" }); } diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index 052d83b..6c571c5 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -472,6 +472,9 @@ export function CallProvider(props : CallProviderProps) { id: generateRandomKey(16), preview: duration.toString(), type: AttachmentType.CALL, + transport_server: "", + transport_tag: "", + encoded_for: "", blob: "" }], true); } diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 6d646fd..5882f59 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -46,6 +46,9 @@ export interface AttachmentMeta { id: string; type: AttachmentType; preview: string; + transport_tag: string; + encoded_for: string; + transport_server: string; } export interface Message { @@ -469,9 +472,7 @@ export function DialogProvider(props: DialogProviderProps) { 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, + ...attachment, blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob) : "" }); } @@ -549,9 +550,7 @@ export function DialogProvider(props: DialogProviderProps) { 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, + ...attachment, blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : "" }); } @@ -627,9 +626,7 @@ export function DialogProvider(props: DialogProviderProps) { 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, + ...attachment, blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob) : "" }); } @@ -707,9 +704,7 @@ export function DialogProvider(props: DialogProviderProps) { 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, + ...attachment, blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : "" }); } @@ -964,7 +959,10 @@ export function DialogProvider(props: DialogProviderProps) { id: meta.id, blob: blob, type: meta.type, - preview: meta.preview + preview: meta.preview, + transport_server: meta.transport_server, + transport_tag: meta.transport_tag, + encoded_for: meta.encoded_for }); } return attachments; diff --git a/app/providers/DialogProvider/useDialog.ts b/app/providers/DialogProvider/useDialog.ts index 61c5b63..c9c1bb7 100644 --- a/app/providers/DialogProvider/useDialog.ts +++ b/app/providers/DialogProvider/useDialog.ts @@ -118,7 +118,10 @@ export function useDialog() : { attachmentsMeta.push({ id: attachment.id, type: attachment.type, - preview: attachment.preview + preview: attachment.preview, + transport_server: attachment.transport_server, + transport_tag: attachment.transport_tag, + encoded_for: dialog }); if(attachment.type == AttachmentType.FILE){ /** diff --git a/app/providers/DialogProvider/useSynchronize.ts b/app/providers/DialogProvider/useSynchronize.ts index 82c5bef..1aff118 100644 --- a/app/providers/DialogProvider/useSynchronize.ts +++ b/app/providers/DialogProvider/useSynchronize.ts @@ -20,7 +20,7 @@ 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 { AttachmentMeta, DeliveredMessageState, Message } from "./DialogProvider"; import { MESSAGE_MAX_LOADED, TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD } from "@/app/constants"; import { useMemory } from "../MemoryProvider/useMemory"; import { useDialogsCache } from "./useDialogsCache"; @@ -165,7 +165,7 @@ export function useSynchronize() { const nonce = chachaDecryptedKey.slice(32); const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); await updateSyncTime(timestamp); - let attachmentsMeta: any[] = []; + let attachmentsMeta: AttachmentMeta[] = []; let messageAttachments: Attachment[] = []; for (let i = 0; i < packet.getAttachments().length; i++) { const attachment = packet.getAttachments()[i]; @@ -190,7 +190,10 @@ export function useSynchronize() { attachmentsMeta.push({ id: attachment.id, type: attachment.type, - preview: attachment.preview + preview: attachment.preview, + encoded_for: attachment.encoded_for, + transport_server: attachment.transport_server, + transport_tag: attachment.transport_tag }); } @@ -347,7 +350,7 @@ export function useSynchronize() { decryptedContent = ''; } - let attachmentsMeta: any[] = []; + let attachmentsMeta: AttachmentMeta[] = []; let messageAttachments: Attachment[] = []; for (let i = 0; i < packet.getAttachments().length; i++) { const attachment = packet.getAttachments()[i]; @@ -372,7 +375,10 @@ export function useSynchronize() { attachmentsMeta.push({ id: attachment.id, type: attachment.type, - preview: attachment.preview + preview: attachment.preview, + encoded_for: attachment.encoded_for, + transport_server: attachment.transport_server, + transport_tag: attachment.transport_tag }); } diff --git a/app/providers/TransportProvider/TransportProvider.tsx b/app/providers/TransportProvider/TransportProvider.tsx index ef411dc..a009c59 100644 --- a/app/providers/TransportProvider/TransportProvider.tsx +++ b/app/providers/TransportProvider/TransportProvider.tsx @@ -7,7 +7,7 @@ import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; interface TransportContextValue { transportServer: string | null; uploadFile: (id: string, content: string) => Promise; - downloadFile: (id: string, tag: string) => Promise; + downloadFile: (id: string, tag: string, transportServer: string) => Promise; uploading: TransportState[]; downloading: TransportState[]; } @@ -86,14 +86,14 @@ export function TransportProvider(props: TransportProviderProps) { * @param tag тег файла * @param chachaDecryptedKey ключ для расшифровки файла */ - const downloadFile = (id: string, tag : string) : Promise => { + const downloadFile = (id: string, tag : string, transportServer: string) : Promise => { return new Promise((resolve, reject) => { - if (!transportServerRef.current) { + if (!transportServer) { throw new Error("Transport server is not set"); } setDownloading(prev => [...prev, { id: id, progress: 0 }]); const xhr = new XMLHttpRequest(); - xhr.open('GET', `${transportServerRef.current}/d/${tag}`); + xhr.open('GET', `${transportServer}/d/${tag}`); xhr.responseType = 'text'; xhr.onprogress = (event) => { diff --git a/app/providers/TransportProvider/useTransportServer.ts b/app/providers/TransportProvider/useTransportServer.ts new file mode 100644 index 0000000..9caa807 --- /dev/null +++ b/app/providers/TransportProvider/useTransportServer.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import { TransportContext } from "./TransportProvider"; + +export function useTransportServer() { + const context = useContext(TransportContext); + if(!context){ + throw new Error("useTransportServer must be used within a TransportProvider"); + } + const { transportServer } = context; + + return transportServer; +} \ No newline at end of file diff --git a/app/servers.ts b/app/servers.ts index c5779c3..ce396d3 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 { diff --git a/lib/main/main.ts b/lib/main/main.ts index 773265d..6ef6fc8 100644 --- a/lib/main/main.ts +++ b/lib/main/main.ts @@ -18,109 +18,128 @@ const size = process.platform === 'darwin' ? 18 : 22 const logger = Logger('main') const icon = nativeImage - .createFromPath(join(__dirname, '../../resources/R.png')) - .resize({ width: size, height: size }) + .createFromPath(join(__dirname, '../../resources/R.png')) + .resize({ width: size, height: size }) if (!lockInstance) { - app.quit() - process.exit(0) + app.quit() + process.exit(0) } process.on('unhandledRejection', (reason) => { - logger.log(`main thread error, reason: ${reason}`) + logger.log(`main thread error, reason: ${reason}`) }) app.disableHardwareAcceleration() app.on('second-instance', () => { - const allWindows = BrowserWindow.getAllWindows() - if (allWindows.length) { - const mainWindow = allWindows[0] - if (mainWindow.isMinimized()) mainWindow.restore() - if (!mainWindow.isVisible()) mainWindow.show() - mainWindow.focus() - } + const allWindows = BrowserWindow.getAllWindows() + if (allWindows.length) { + const mainWindow = allWindows[0] + if (mainWindow.isMinimized()) mainWindow.restore() + if (!mainWindow.isVisible()) mainWindow.show() + mainWindow.focus() + } }) export const restoreApplicationAfterClickOnTrayOrDock = () => { - const allWindows = BrowserWindow.getAllWindows() - if (allWindows.length > 0) { - const mainWindow = allWindows[0] - if (mainWindow.isMinimized()) { - mainWindow.restore() - return - } - if (!mainWindow.isVisible()) { - mainWindow.show() - } - mainWindow.focus() - } else { - createAppWindow() - } + const allWindows = BrowserWindow.getAllWindows() + if (allWindows.length > 0) { + const mainWindow = allWindows[0] + if (mainWindow.isMinimized()) { + mainWindow.restore() + return + } + if (!mainWindow.isVisible()) { + mainWindow.show() + } + mainWindow.focus() + } else { + createAppWindow() + } } app.whenReady().then(async () => { - electronApp.setAppUserModelId('Rosetta') + electronApp.setAppUserModelId('Rosetta') - // Убираем File/View и оставляем только app + минимальный Edit (roles) - if (process.platform === 'darwin') { - const minimalMenu = Menu.buildFromTemplate([ - { - label: app.name, - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' } - ] - }, - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'pasteAndMatchStyle' }, - { role: 'delete' }, - { role: 'selectAll' } - ] - } - ]) - Menu.setApplicationMenu(minimalMenu) - } else { - Menu.setApplicationMenu(null) - } + // Убираем File/View и оставляем только app + минимальный Edit (roles) + if (process.platform === 'darwin') { + const minimalMenu = Menu.buildFromTemplate([ + { + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'pasteAndMatchStyle' }, + { role: 'delete' }, + { role: 'selectAll' } + ] + } + ]) + Menu.setApplicationMenu(minimalMenu) + } else { + Menu.setApplicationMenu(null) + } - tray = new Tray(icon) - const contextMenu = Menu.buildFromTemplate([ - { label: 'Open App', click: () => restoreApplicationAfterClickOnTrayOrDock() }, - { label: 'Quit', click: () => app.quit() } - ]) - tray.setContextMenu(contextMenu) - tray.setToolTip('Rosetta') - tray.on('click', () => { - restoreApplicationAfterClickOnTrayOrDock() - }) + tray = new Tray(icon) + const contextMenu = Menu.buildFromTemplate([ + { label: 'Open App', click: () => restoreApplicationAfterClickOnTrayOrDock() }, + { label: 'Quit', click: () => app.quit() } + ]) + tray.setContextMenu(contextMenu) + tray.setToolTip('Rosetta') + tray.on('click', () => { + restoreApplicationAfterClickOnTrayOrDock() + }) - startApplication() + startApplication() - app.on('browser-window-created', (_, window) => { - optimizer.watchWindowShortcuts(window) - }) + const isDevBuild = + !app.isPackaged || + process.env.NODE_ENV === 'development' || + Boolean(process.env.ELECTRON_RENDERER_URL) - app.on('activate', () => { - restoreApplicationAfterClickOnTrayOrDock() - }) + app.on('browser-window-created', (_, window) => { + // В production оставляем стандартную защиту шорткатов + if (!isDevBuild) { + optimizer.watchWindowShortcuts(window) + return + } + + // В dev явно разрешаем Ctrl+R и Cmd+R для перезагрузки, так как в режиме разработки это часто нужно + window.webContents.on('before-input-event', (event, input) => { + const key = input.key?.toLowerCase?.() ?? '' + const isReload = input.type === 'keyDown' && (input.meta || input.control) && key === 'r' + if (isReload) { + event.preventDefault() + window.webContents.reloadIgnoringCache() + } + }) + }) + + app.on('activate', () => { + restoreApplicationAfterClickOnTrayOrDock() + }) }) app.on('window-all-closed', () => { - if (process.platform === 'darwin') { - app.hide() - } + if (process.platform === 'darwin') { + app.hide() + } }) \ No newline at end of file