From bb55fb47aad3d22eda0a97aff7467eb12acb8fb0 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 18 Feb 2026 20:41:37 +0200 Subject: [PATCH 01/15] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/service-packs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/service-packs.yaml b/.gitea/workflows/service-packs.yaml index 008d538..2255952 100644 --- a/.gitea/workflows/service-packs.yaml +++ b/.gitea/workflows/service-packs.yaml @@ -7,7 +7,7 @@ on: branches: - main paths: - - 'lib/**' + - 'app/**' jobs: build: From f959c6335c8abd18ab3936b378486df8749d51e3 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 19:31:31 +0200 Subject: [PATCH 02/15] =?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=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=20=D0=B2=20=D1=83=D0=BF=D0=BE=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/MentionList/MentionRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/MentionList/MentionRow.tsx b/app/components/MentionList/MentionRow.tsx index e51208f..742be56 100644 --- a/app/components/MentionList/MentionRow.tsx +++ b/app/components/MentionList/MentionRow.tsx @@ -19,8 +19,8 @@ export function MentionRow(props : MentionRowProps) { {props.username == 'all' && @} {props.username == 'admin' && @} {props.username != 'all' && props.username != 'admin' && 0 ? avatars[0].avatar : null} >} From ff96dfd20485dc996a6a37f05d64be2a80930e1a Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 19:37:37 +0200 Subject: [PATCH 03/15] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D1=88=D0=B0=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=BE=D0=BF?= =?UTF-8?q?=D1=8B=D1=82=D0=BA=D0=B5=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BD=D0=B0=20=D1=81=D0=B8=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=D0=BD=D0=BE=D0=B5=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/DialogInput/DialogInput.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/components/DialogInput/DialogInput.tsx b/app/components/DialogInput/DialogInput.tsx index 4f3977c..f1c3aef 100644 --- a/app/components/DialogInput/DialogInput.tsx +++ b/app/components/DialogInput/DialogInput.tsx @@ -229,6 +229,12 @@ export function DialogInput() { } const onPaste = async (event: React.ClipboardEvent) => { + if(systemAccounts.find((acc) => acc.publicKey == dialog)){ + /** + * У системных аккаунтов нельзя вызывать вложения + */ + return; + } if(attachments.length >= MAX_ATTACHMENTS_IN_MESSAGE){ return; } @@ -249,7 +255,9 @@ export function DialogInput() { preview: await base64ImageToBlurhash(base64Image) }]); } - editableDivRef.current.focus(); + if(editableDivRef.current){ + editableDivRef.current.focus(); + } break; } } From a0c73c807f4445bf808ca16e13945a7627294c5c Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 19:46:10 +0200 Subject: [PATCH 04/15] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D1=88=D0=B0=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=BE=D0=BF?= =?UTF-8?q?=D1=8B=D1=82=D0=BA=D0=B5=20=D0=B2=D1=81=D1=82=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/DialogInput/DialogInput.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/components/DialogInput/DialogInput.tsx b/app/components/DialogInput/DialogInput.tsx index f1c3aef..9314207 100644 --- a/app/components/DialogInput/DialogInput.tsx +++ b/app/components/DialogInput/DialogInput.tsx @@ -104,6 +104,12 @@ export function DialogInput() { }, [dialog, editableDivRef]); useEffect(() => { + if(systemAccounts.find((acc) => acc.publicKey == dialog)){ + /** + * У системных аккаунтов нельзя отвечать на сообщения + */ + return; + } if(replyMessages.inDialogInput && replyMessages.inDialogInput == dialog){ setAttachments([{ type: AttachmentType.MESSAGES, @@ -111,7 +117,9 @@ export function DialogInput() { blob: JSON.stringify([...replyMessages.messages]), preview: "" }]); - editableDivRef.current.focus(); + if(editableDivRef.current){ + editableDivRef.current.focus(); + } } }, [dialog, replyMessages]); @@ -208,6 +216,12 @@ export function DialogInput() { } const onClickCamera = async () => { + if(systemAccounts.find((acc) => acc.publicKey == dialog)){ + /** + * У системных аккаунтов нельзя вызывать вложения + */ + return; + } if(avatars.length == 0){ return; } @@ -217,7 +231,9 @@ export function DialogInput() { type: AttachmentType.AVATAR, preview: await base64ImageToBlurhash(avatars[0].avatar) }]); - editableDivRef.current.focus(); + if(editableDivRef.current){ + editableDivRef.current.focus(); + } } const sendTypeingPacket = () => { From 026a3c9520f9c2bb7239ac284d7c8bb038e7d4ca Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 20:52:27 +0200 Subject: [PATCH 05/15] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B3=D0=B0=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BA=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BB=D0=B0=20=D0=B2=20=D1=81=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=D1=85,=20=D0=B1=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=87=D0=BD=D0=BE=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=8B=D0=BB?= =?UTF-8?q?=D0=BA=D0=B5=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/Messages/Messages.tsx | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/app/components/Messages/Messages.tsx b/app/components/Messages/Messages.tsx index 44b1a6c..8ee6b21 100644 --- a/app/components/Messages/Messages.tsx +++ b/app/components/Messages/Messages.tsx @@ -7,14 +7,12 @@ import { MessageSkeleton } from "../MessageSkeleton/MessageSkeleton"; import { ScrollArea } from "@mantine/core"; import { MESSAGE_AVATAR_NO_RENDER_TIME_DIFF_S, SCROLL_TOP_IN_MESSAGES_TO_VIEW_AFFIX } from "@/app/constants"; import { DialogAffix } from "../DialogAffix/DialogAffix"; -import { useReplyMessages } from "@/app/providers/DialogProvider/useReplyMessages"; import { useSetting } from "@/app/providers/SettingsProvider/useSetting"; export function Messages() { const colors = useRosettaColors(); const publicKey = usePublicKey(); const { messages, dialog, loadMessagesToTop, loading } = useDialog(); - const { replyMessages, isSelectionStarted } = useReplyMessages(); const viewportRef = useRef(null); const lastMessageRef = useRef(null); @@ -22,6 +20,7 @@ export function Messages() { const shouldAutoScrollRef = useRef(true); const isFirstRenderRef = useRef(true); const previousScrollHeightRef = useRef(0); + const distanceFromButtomRef = useRef(0); const [affix, setAffix] = useState(false); const [wallpaper] = useSetting @@ -121,15 +120,10 @@ export function Messages() { const lastMessage = messages[messages.length - 1]; // Скроллим если пользователь внизу или это его собственное сообщение - if ((shouldAutoScrollRef.current || lastMessage.from_me) && !affix) { - /** - * Скролл только если пользователь не читает сейчас старую переписку - * (!affix)) - */ - //console.info("Scroll because", shouldAutoScrollRef.current); + if ((shouldAutoScrollRef.current || lastMessage.from_me)) { scrollToBottom(true); } - }, [messages.length, loading, affix, scrollToBottom]); + }, [messages.length, loading, scrollToBottom]); // Восстановление позиции после загрузки старых сообщений useEffect(() => { @@ -142,12 +136,6 @@ export function Messages() { } }, [messages.length]); - // Скролл при отправке reply сообщения - useEffect(() => { - if (replyMessages.messages.length === 0 || isSelectionStarted()) return; - scrollToBottom(true); - }, [replyMessages.messages.length]); - const loadMessagesToScrollAreaTop = async () => { if (!viewportRef.current) return; @@ -195,6 +183,7 @@ export function Messages() { // Показываем/скрываем кнопку "вниз" const distanceFromBottom = (viewportRef.current.scrollHeight - viewportRef.current.clientHeight) - scroll.y; + distanceFromButtomRef.current = distanceFromBottom; setAffix(distanceFromBottom > SCROLL_TOP_IN_MESSAGES_TO_VIEW_AFFIX); }} From 6908dd486cf600419e824104b0f6d6cf5678c28f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 21:27:58 +0200 Subject: [PATCH 06/15] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8=20=D0=B2=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B5=20=D0=BA=D1=8D?= =?UTF-8?q?=D1=88=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D1=8B=D1=85=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/linux.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/linux.yaml b/.gitea/workflows/linux.yaml index ed1951a..dadad96 100644 --- a/.gitea/workflows/linux.yaml +++ b/.gitea/workflows/linux.yaml @@ -26,9 +26,9 @@ jobs: uses: actions/cache@v5 with: path: ${{ env.HOME }}/.npm-cache - key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }} + key: ${{ runner.os }}-npm-linux-${{ hashFiles('**/package.json') }} restore-keys: | - ${{ runner.os }}-npm- + ${{ runner.os }}-npm-linux- if-no-files-found: ignore # Кэш для electron-builder - name: Cache electron-builder @@ -37,9 +37,9 @@ jobs: path: | ${{ env.HOME }}/Library/Caches/electron-builder ${{ env.HOME }}/Library/Caches/electron - key: ${{ runner.os }}-electron-builder-${{ hashFiles('**/electron-builder.yml') }} + key: ${{ runner.os }}-electron-linux-builder-${{ hashFiles('**/electron-builder.yml') }} restore-keys: | - ${{ runner.os }}-electron-builder- + ${{ runner.os }}-electron-linux-builder- if-no-files-found: ignore - name: NPM offline setup shell: bash From a38a331cd1382ed447a3d30d8e95dd0311e0effa Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 21:30:31 +0200 Subject: [PATCH 07/15] =?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=D0=B8=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D1=81=D0=B5=D0=BA=D1=80=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B2,=20=D0=BF=D0=BE=D0=B2=D1=8B=D1=88=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/darwin.yaml | 16 ++++++++-------- .gitea/workflows/linux.yaml | 16 ++++++++-------- .gitea/workflows/service-packs.yaml | 6 +++--- .gitea/workflows/windows.yaml | 8 ++++---- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.gitea/workflows/darwin.yaml b/.gitea/workflows/darwin.yaml index 89705c4..8f06623 100644 --- a/.gitea/workflows/darwin.yaml +++ b/.gitea/workflows/darwin.yaml @@ -59,14 +59,14 @@ jobs: 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.SSH_TARGET_DIR }}/darwin/x64" \ - -s "${{ secrets.SSH_HOST }}" \ - -u "${{ secrets.SSH_USERNAME }}" \ - -p '${{ secrets.SSH_PASSWORD }}' + -r "${{ secrets.SDU_SSH_TARGET_DIR }}/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.SSH_TARGET_DIR }}/darwin/arm64" \ - -s "${{ secrets.SSH_HOST }}" \ - -u "${{ secrets.SSH_USERNAME }}" \ - -p '${{ secrets.SSH_PASSWORD }}' + -r "${{ secrets.SDU_SSH_TARGET_DIR }}/darwin/arm64" \ + -s "${{ secrets.SDU_SSH_HOST }}" \ + -u "${{ secrets.SDU_SSH_USERNAME }}" \ + -p '${{ secrets.SDU_SSH_PASSWORD }}' \ No newline at end of file diff --git a/.gitea/workflows/linux.yaml b/.gitea/workflows/linux.yaml index dadad96..f57760d 100644 --- a/.gitea/workflows/linux.yaml +++ b/.gitea/workflows/linux.yaml @@ -59,14 +59,14 @@ jobs: chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \ -l "$GITHUB_WORKSPACE/dist/builds/linux/x64/Rosetta-*.AppImage" \ - -r "${{ secrets.SSH_TARGET_DIR }}/linux/x64" \ - -s "${{ secrets.SSH_HOST }}" \ - -u "${{ secrets.SSH_USERNAME }}" \ - -p '${{ secrets.SSH_PASSWORD }}' + -r "${{ secrets.SDU_SSH_TARGET_DIR }}/linux/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/linux/arm64/Rosetta-*.AppImage" \ - -r "${{ secrets.SSH_TARGET_DIR }}/linux/arm64" \ - -s "${{ secrets.SSH_HOST }}" \ - -u "${{ secrets.SSH_USERNAME }}" \ - -p '${{ secrets.SSH_PASSWORD }}' + -r "${{ secrets.SDU_SSH_TARGET_DIR }}/linux/arm64" \ + -s "${{ secrets.SDU_SSH_HOST }}" \ + -u "${{ secrets.SDU_SSH_USERNAME }}" \ + -p '${{ secrets.SDU_SSH_PASSWORD }}' \ No newline at end of file diff --git a/.gitea/workflows/service-packs.yaml b/.gitea/workflows/service-packs.yaml index 2255952..864a525 100644 --- a/.gitea/workflows/service-packs.yaml +++ b/.gitea/workflows/service-packs.yaml @@ -69,6 +69,6 @@ jobs: sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \ -l "$GITHUB_WORKSPACE/packs/*" \ -r "${{ secrets.SDU_SSH_PACKS }}" \ - -s "${{ secrets.SSH_HOST }}" \ - -u "${{ secrets.SSH_USERNAME }}" \ - -p '${{ secrets.SSH_PASSWORD }}' \ No newline at end of file + -s "${{ secrets.SDU_SSH_HOST }}" \ + -u "${{ secrets.SDU_SSH_USERNAME }}" \ + -p '${{ secrets.SDU_SSH_PASSWORD }}' \ No newline at end of file diff --git a/.gitea/workflows/windows.yaml b/.gitea/workflows/windows.yaml index a42c27b..2fffde3 100644 --- a/.gitea/workflows/windows.yaml +++ b/.gitea/workflows/windows.yaml @@ -53,7 +53,7 @@ jobs: run: | & "$env:GITHUB_WORKSPACE\.gitea\workflows\sshupload.ps1" ` -LocalFilePath "dist/builds/win/x64/Rosetta-*.exe" ` - -RemoteFolderPath "${{ secrets.SSH_TARGET_DIR }}/win32/x64" ` - -ServerAddress "${{ secrets.SSH_HOST }}" ` - -Username "${{ secrets.SSH_USERNAME }}" ` - -PasswordParam '${{ secrets.SSH_PASSWORD }}' + -RemoteFolderPath "${{ secrets.SDU_SSH_TARGET_DIR }}/win32/x64" ` + -ServerAddress "${{ secrets.SDU_SSH_HOST }}" ` + -Username "${{ secrets.SDU_SSH_USERNAME }}" ` + -PasswordParam '${{ secrets.SDU_SSH_PASSWORD }}' From 53535d68e0ed15a0eb86d117b6a0d74c76772997 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 21:43:27 +0200 Subject: [PATCH 08/15] =?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=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=B2=D1=81=D1=82=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9,=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=BE=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/DialogInput/DialogInput.tsx | 12 ++-- .../MessageAttachments/MessageAvatar.tsx | 12 +++- .../MessageAttachments/MessageImage.tsx | 13 +++- .../DialogProvider/DialogProvider.tsx | 9 +++ app/utils/utils.ts | 45 ------------ app/workers/image/image.ts | 39 ++++++++++ app/workers/image/image.worker.ts | 71 +++++++++++++++++++ 7 files changed, 147 insertions(+), 54 deletions(-) create mode 100644 app/workers/image/image.ts create mode 100644 app/workers/image/image.worker.ts diff --git a/app/components/DialogInput/DialogInput.tsx b/app/components/DialogInput/DialogInput.tsx index 9314207..e529a4e 100644 --- a/app/components/DialogInput/DialogInput.tsx +++ b/app/components/DialogInput/DialogInput.tsx @@ -4,7 +4,7 @@ import { Box, Divider, Flex, Menu, Popover, Text, Transition, useComputedColorSc import { IconBarrierBlock, IconCamera, IconDoorExit, IconFile, IconMoodSmile, IconPaperclip, IconSend } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist"; -import { base64ImageToBlurhash, filePrapareForNetworkTransfer, generateRandomKey, imagePrepareForNetworkTransfer } from "@/app/utils/utils"; +import { filePrapareForNetworkTransfer, generateRandomKey, imagePrepareForNetworkTransfer } from "@/app/utils/utils"; import { Attachment, AttachmentType } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message"; import { DialogAttachment } from "../DialogAttachment/DialogAttachment"; import { PacketTyping } from "@/app/providers/ProtocolProvider/protocol/packets/packet.typeing"; @@ -229,7 +229,7 @@ export function DialogInput() { blob: avatars[0].avatar, id: generateRandomKey(8), type: AttachmentType.AVATAR, - preview: await base64ImageToBlurhash(avatars[0].avatar) + preview: "" }]); if(editableDivRef.current){ editableDivRef.current.focus(); @@ -264,11 +264,12 @@ export function DialogInput() { const file = item.getAsFile(); if (file) { const base64Image = await imagePrepareForNetworkTransfer(file); + const attachmentId = generateRandomKey(8); setAttachments([...attachments, { blob: base64Image, - id: generateRandomKey(8), + id: attachmentId, type: AttachmentType.IMAGE, - preview: await base64ImageToBlurhash(base64Image) + preview: "" }]); } if(editableDivRef.current){ @@ -297,9 +298,10 @@ export function DialogInput() { return; } let fileContent = await filePrapareForNetworkTransfer(file); + const attachmentId = generateRandomKey(8); setAttachments([...attachments, { blob: fileContent, - id: generateRandomKey(8), + id: attachmentId, type: AttachmentType.FILE, preview: files[0].size + "::" + files[0].name }]); diff --git a/app/components/MessageAttachments/MessageAvatar.tsx b/app/components/MessageAttachments/MessageAvatar.tsx index 5523635..99b1e01 100644 --- a/app/components/MessageAttachments/MessageAvatar.tsx +++ b/app/components/MessageAttachments/MessageAvatar.tsx @@ -4,12 +4,12 @@ import { AspectRatio, Button, Flex, Paper, Text } from "@mantine/core"; import { IconArrowDown } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { AttachmentProps } from "./MessageAttachments"; -import { blurhashToBase64Image } from "@/app/utils/utils"; import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress"; import { ImageToView } from "@/app/providers/ImageViewerProvider/ImageViewerProvider"; import { DownloadStatus, useAttachment } from "@/app/providers/AttachmentProvider/useAttachment"; import { PopoverLockIconAvatar } from "../PopoverLockIconAvatar/PopoverLockIconAvatar"; import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints"; +import { blurhashToBase64Image } from "@/app/workers/image/image"; export function MessageAvatar(props: AttachmentProps) { const colors = useRosettaColors(); @@ -25,10 +25,12 @@ export function MessageAvatar(props: AttachmentProps) { const preview = getPreview(); const [blob, setBlob] = useState(props.attachment.blob); const {lg} = useRosettaBreakpoints(); + const [blurhashPreview, setBlurhashPreview] = useState(""); useEffect(() => { constructBlob(); + constructFromBlurhash(); }, [downloadStatus]); const constructBlob = async () => { @@ -57,6 +59,12 @@ export function MessageAvatar(props: AttachmentProps) { } } + const constructFromBlurhash = async () => { + if (preview.length < 20) return; + const blob = await blurhashToBase64Image(preview, 200, 220); + setBlurhashPreview(blob); + } + return ( @@ -79,7 +87,7 @@ export function MessageAvatar(props: AttachmentProps) { height: 60, borderRadius: '50%', objectFit: 'cover' - }} src={/*block render???*/blurhashToBase64Image(preview, 200, 220)}> + }} src={blurhashPreview == "" ? undefined : blurhashPreview}> )} diff --git a/app/components/MessageAttachments/MessageImage.tsx b/app/components/MessageAttachments/MessageImage.tsx index 5e22afe..e04ea40 100644 --- a/app/components/MessageAttachments/MessageImage.tsx +++ b/app/components/MessageAttachments/MessageImage.tsx @@ -5,10 +5,11 @@ import { AspectRatio, Box, Flex, Overlay, Portal, Text } from "@mantine/core"; import { IconArrowDown, IconCircleX, IconFlameFilled } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { AttachmentProps } from "./MessageAttachments"; -import { blurhashToBase64Image, isMessageDeliveredByTime } from "@/app/utils/utils"; +import { isMessageDeliveredByTime } from "@/app/utils/utils"; import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress"; import { ImageToView } from "@/app/providers/ImageViewerProvider/ImageViewerProvider"; import { DownloadStatus, useAttachment } from "@/app/providers/AttachmentProvider/useAttachment"; +import { blurhashToBase64Image } from "@/app/workers/image/image"; export function MessageImage(props: AttachmentProps) { const colors = useRosettaColors(); @@ -25,9 +26,11 @@ export function MessageImage(props: AttachmentProps) { const preview = getPreview(); const [blob, setBlob] = useState(props.attachment.blob); const [loadedImage, setLoadedImage] = useState(false); + const [blurhashPreview, setBlurhashPreview] = useState(""); useEffect(() => { constructBlob(); + constructFromBlurhash(); }, [downloadStatus]); const constructBlob = async () => { @@ -45,6 +48,12 @@ export function MessageImage(props: AttachmentProps) { open(images, 0); } + const constructFromBlurhash = async () => { + if (preview.length < 20) return; + const blob = await blurhashToBase64Image(preview, 200, 220); + setBlurhashPreview(blob); + } + const onClick = () => { if (downloadStatus == DownloadStatus.DOWNLOADED) { openImageViewer(); @@ -83,7 +92,7 @@ export function MessageImage(props: AttachmentProps) { borderRadius: 8, objectFit: 'cover', border: '1px solid ' + colors.borderColor - }} src={/*block render???*/blurhashToBase64Image(preview, 200, 220)}> + }} src={blurhashPreview == "" ? undefined : blurhashPreview}> )} diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 448edf4..7aed63d 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -25,6 +25,7 @@ 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; @@ -923,6 +924,14 @@ export function DialogProvider(props: DialogProviderProps) { }); 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; diff --git a/app/utils/utils.ts b/app/utils/utils.ts index 34b2d40..8278712 100644 --- a/app/utils/utils.ts +++ b/app/utils/utils.ts @@ -1,6 +1,5 @@ import { MantineColor } from "@mantine/core"; import { MESSAGE_MAX_TIME_TO_DELEVERED_S } from "../constants"; -import { decode, encode } from "blurhash"; export function generateRandomKey(length: number): string { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -243,47 +242,3 @@ export function isImage(blob : string) : boolean { } return blob.startsWith('data:image/'); } - -export function blurhashToBase64Image(blurhash: string, width: number, height: number): string { - const pixels = decode(blurhash, width, height); - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - const imageData = ctx?.createImageData(width, height); - if (imageData) { - imageData.data.set(pixels); - ctx?.putImageData(imageData, 0, 0); - return canvas.toDataURL(); - } - return ''; -} - -export function base64ImageToBlurhash(base64Image: string): Promise { - const img = new Image(); - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - return new Promise((resolve, reject) => { - img.onload = () => { - canvas.width = img.width; - canvas.height = img.height; - ctx?.drawImage(img, 0, 0); - const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height); - if (imageData) { - const blurhash = encode( - imageData.data, - imageData.width, - imageData.height, - 4, - 4 - ); - resolve(blurhash); - } else { - reject('Failed to get image data from canvas.'); - } - }; - img.onerror = (error) => reject(error); - img.src = base64Image; - }); -} \ No newline at end of file diff --git a/app/workers/image/image.ts b/app/workers/image/image.ts new file mode 100644 index 0000000..33f3651 --- /dev/null +++ b/app/workers/image/image.ts @@ -0,0 +1,39 @@ +// ...existing code... +const worker = new Worker(new URL('./image.worker.ts', import.meta.url), { type: 'module' }); + +type WorkerReq = + | { id: number; type: 'blurhashToBase64Image'; payload: { blurhash: string; width: number; height: number } } + | { id: number; type: 'base64ImageToBlurhash'; payload: { base64Image: string } }; + +type WorkerRes = + | { id: number; ok: true; data: string } + | { id: number; ok: false; error: string }; + +let seq = 0; +const pending = new Map void>(); + +worker.onmessage = (e: MessageEvent) => { + const res = e.data; + const cb = pending.get(res.id); + if (cb) { + pending.delete(res.id); + cb(res); + } +}; + +function callWorker(req: Omit): Promise { + return new Promise((resolve, reject) => { + const id = ++seq; + pending.set(id, (res) => (res.ok ? resolve(res.data) : reject(res.error))); + worker.postMessage({ ...req, id }); + }); +} + +export function blurhashToBase64Image(blurhash: string, width: number, height: number): Promise { + return callWorker({ type: 'blurhashToBase64Image', payload: { blurhash, width, height } }); +} + +export function base64ImageToBlurhash(base64Image: string): Promise { + return callWorker({ type: 'base64ImageToBlurhash', payload: { base64Image } }); +} +// ...existing code... \ No newline at end of file diff --git a/app/workers/image/image.worker.ts b/app/workers/image/image.worker.ts new file mode 100644 index 0000000..6d5842a --- /dev/null +++ b/app/workers/image/image.worker.ts @@ -0,0 +1,71 @@ +import { decode, encode } from 'blurhash'; + +type Req = + | { id: number; type: 'blurhashToBase64Image'; payload: { blurhash: string; width: number; height: number } } + | { id: number; type: 'base64ImageToBlurhash'; payload: { base64Image: string } }; + +type Res = + | { id: number; ok: true; data: string } + | { id: number; ok: false; error: string }; + +const toBase64 = async (blurhash: string, width: number, height: number): Promise => { + const pixels = decode(blurhash, width, height); + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('No 2d context'); + const imageData = ctx.createImageData(width, height); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + const blob = await canvas.convertToBlob({ type: 'image/png' }); + const buf = new Uint8Array(await blob.arrayBuffer()); + let bin = ''; + for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]); + return `data:image/png;base64,${btoa(bin)}`; +}; + +const toBlurhash = async (base64Image: string): Promise => { + const src = base64Image?.trim(); + if (!src) throw new Error('Empty image data'); + + const resp = await fetch(src); + const blob = await resp.blob(); + if (!blob.size) throw new Error('Image fetch returned empty blob'); + + const bitmap = await createImageBitmap(blob); + const { width, height } = bitmap; + if (!width || !height) { + bitmap.close(); + throw new Error(`Image has invalid size ${width}x${height}`); + } + + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + if (!ctx) { + bitmap.close(); + throw new Error('No 2d context'); + } + + ctx.drawImage(bitmap, 0, 0, width, height); + bitmap.close(); + + const imageData = ctx.getImageData(0, 0, width, height); + return encode(imageData.data, imageData.width, imageData.height, 4, 4); +}; + +self.onmessage = async (e: MessageEvent) => { + const { id, type, payload } = e.data; + const reply = (res: Res) => (self as unknown as Worker).postMessage(res); + try { + if (type === 'blurhashToBase64Image') { + const data = await toBase64(payload.blurhash, payload.width, payload.height); + reply({ id, ok: true, data }); + } else if (type === 'base64ImageToBlurhash') { + const data = await toBlurhash(payload.base64Image); + reply({ id, ok: true, data }); + } else { + throw new Error(`Unknown type ${type}`); + } + } catch (err: any) { + reply({ id, ok: false, error: String(err?.message ?? err) }); + } +}; \ No newline at end of file From 95a1f573814fab3d04444c694b4005f14de3d040 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 21:50:53 +0200 Subject: [PATCH 09/15] =?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=87=D0=B8=D1=82=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=BA=D0=BE=D0=B4=D0=B0,=20?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=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=B0=D1=80=D1=85?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/providers/AttachmentProvider/useAttachment.ts | 2 +- app/providers/AvatarProvider/AvatarProvider.tsx | 2 +- app/providers/DialogListProvider/useDialogInfo.ts | 2 +- app/providers/DialogProvider/DialogProvider.tsx | 2 +- app/providers/DialogProvider/useDialog.ts | 2 +- app/providers/DialogProvider/useDialogFiber.ts | 2 +- app/providers/DialogProvider/useGroups.ts | 2 +- app/providers/SystemAccountsProvider/SystemAccountsProvider.tsx | 2 +- app/providers/SystemProvider/SystemProvider.tsx | 2 +- app/views/Backup/Backup.tsx | 2 +- app/views/Lockscreen/Lockscreen.tsx | 2 +- app/views/SetPassword/SetPassword.tsx | 2 +- app/{ => workers}/crypto/crypto.ts | 2 +- app/{ => workers}/crypto/crypto.worker.ts | 0 14 files changed, 13 insertions(+), 13 deletions(-) rename app/{ => workers}/crypto/crypto.ts (98%) rename app/{ => workers}/crypto/crypto.worker.ts (100%) diff --git a/app/providers/AttachmentProvider/useAttachment.ts b/app/providers/AttachmentProvider/useAttachment.ts index f7768f2..d153d60 100644 --- a/app/providers/AttachmentProvider/useAttachment.ts +++ b/app/providers/AttachmentProvider/useAttachment.ts @@ -4,7 +4,7 @@ import { useUploadStatus } from "../TransportProvider/useUploadStatus"; import { useFileStorage } from "../../hooks/useFileStorage"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; -import { decodeWithPassword, encodeWithPassword, generateMd5 } from "../../crypto/crypto"; +import { decodeWithPassword, encodeWithPassword, generateMd5 } from "../../workers/crypto/crypto"; import { useTransport } from "../TransportProvider/useTransport"; import { useDialogsCache } from "../DialogProvider/useDialogsCache"; import { useConsoleLogger } from "../../hooks/useConsoleLogger"; diff --git a/app/providers/AvatarProvider/AvatarProvider.tsx b/app/providers/AvatarProvider/AvatarProvider.tsx index de3e3de..0ef4274 100644 --- a/app/providers/AvatarProvider/AvatarProvider.tsx +++ b/app/providers/AvatarProvider/AvatarProvider.tsx @@ -2,7 +2,7 @@ import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase"; import { createContext, useEffect, useRef, useState } from "react"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { useFileStorage } from "@/app/hooks/useFileStorage"; -import { decodeWithPassword, encodeWithPassword, generateMd5 } from "@/app/crypto/crypto"; +import { decodeWithPassword, encodeWithPassword, generateMd5 } from "@/app/workers/crypto/crypto"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; import { useSystemAccounts } from "../SystemAccountsProvider/useSystemAccounts"; import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants"; diff --git a/app/providers/DialogListProvider/useDialogInfo.ts b/app/providers/DialogListProvider/useDialogInfo.ts index 7684874..cd576bb 100644 --- a/app/providers/DialogListProvider/useDialogInfo.ts +++ b/app/providers/DialogListProvider/useDialogInfo.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { DialogRow } from "./DialogListProvider"; import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase"; import { usePublicKey } from "../AccountProvider/usePublicKey"; -import { decodeWithPassword } from "@/app/crypto/crypto"; +import { decodeWithPassword } from "@/app/workers/crypto/crypto"; import { constructLastMessageTextByAttachments } from "@/app/utils/constructLastMessageTextByAttachments"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; import { DeliveredMessageState, Message } from "../DialogProvider/DialogProvider"; diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 7aed63d..d5fe8b7 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/crypto/crypto'; +import { chacha20Decrypt, decodeWithPassword, decrypt, encodeWithPassword, 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'; diff --git a/app/providers/DialogProvider/useDialog.ts b/app/providers/DialogProvider/useDialog.ts index 5ebfcd5..4b54615 100644 --- a/app/providers/DialogProvider/useDialog.ts +++ b/app/providers/DialogProvider/useDialog.ts @@ -1,6 +1,6 @@ import { useContext } from "react"; import { useDatabase } from "../DatabaseProvider/useDatabase"; -import { chacha20Encrypt, encodeWithPassword, encrypt, generateMd5} from "../../crypto/crypto"; +import { chacha20Encrypt, encodeWithPassword, encrypt, generateMd5} from "../../workers/crypto/crypto"; import { AttachmentMeta, DeliveredMessageState, DialogContext, Message } from "./DialogProvider"; import { Attachment, AttachmentType, PacketMessage } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message"; import { usePublicKey } from "../AccountProvider/usePublicKey"; diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index 0831a27..4ee0a24 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -12,7 +12,7 @@ import { useDialogsCache } from "./useDialogsCache"; import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; import { usePublicKey } from "../AccountProvider/usePublicKey"; -import { chacha20Decrypt, decodeWithPassword, decrypt, encodeWithPassword, generateMd5 } from "@/app/crypto/crypto"; +import { chacha20Decrypt, decodeWithPassword, decrypt, encodeWithPassword, generateMd5 } from "@/app/workers/crypto/crypto"; import { DeliveredMessageState, Message } from "./DialogProvider"; import { PacketRead } from "../ProtocolProvider/protocol/packets/packet.read"; import { PacketDelivery } from "../ProtocolProvider/protocol/packets/packet.delivery"; diff --git a/app/providers/DialogProvider/useGroups.ts b/app/providers/DialogProvider/useGroups.ts index 6f70dc8..78499d6 100644 --- a/app/providers/DialogProvider/useGroups.ts +++ b/app/providers/DialogProvider/useGroups.ts @@ -1,6 +1,6 @@ import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; -import { decodeWithPassword, encodeWithPassword } from "@/app/crypto/crypto"; +import { decodeWithPassword, encodeWithPassword } from "@/app/workers/crypto/crypto"; import { generateRandomKey } from "@/app/utils/utils"; import { useDialogsList } from "../DialogListProvider/useDialogsList"; import { usePublicKey } from "../AccountProvider/usePublicKey"; diff --git a/app/providers/SystemAccountsProvider/SystemAccountsProvider.tsx b/app/providers/SystemAccountsProvider/SystemAccountsProvider.tsx index 9e14da3..8ec8713 100644 --- a/app/providers/SystemAccountsProvider/SystemAccountsProvider.tsx +++ b/app/providers/SystemAccountsProvider/SystemAccountsProvider.tsx @@ -3,7 +3,7 @@ import { createContext } from "react"; import { useSystemAccount } from "./useSystemAccount"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; -import { chacha20Encrypt, encodeWithPassword, encrypt } from "@/app/crypto/crypto"; +import { chacha20Encrypt, encodeWithPassword, encrypt } from "@/app/workers/crypto/crypto"; import { generateRandomKey } from "@/app/utils/utils"; import { DeliveredMessageState } from "../DialogProvider/DialogProvider"; import { UserInformation } from "../InformationProvider/InformationProvider"; diff --git a/app/providers/SystemProvider/SystemProvider.tsx b/app/providers/SystemProvider/SystemProvider.tsx index 906aa06..3ced45c 100644 --- a/app/providers/SystemProvider/SystemProvider.tsx +++ b/app/providers/SystemProvider/SystemProvider.tsx @@ -1,4 +1,4 @@ -import { decodeWithPassword, encodeWithPassword } from "@/app/crypto/crypto"; +import { decodeWithPassword, encodeWithPassword } from "@/app/workers/crypto/crypto"; import { useFileStorage } from "@/app/hooks/useFileStorage"; import { generateRandomKey } from "@/app/utils/utils"; import { createContext, useEffect, useState } from "react"; diff --git a/app/views/Backup/Backup.tsx b/app/views/Backup/Backup.tsx index 411aadf..0cb7317 100644 --- a/app/views/Backup/Backup.tsx +++ b/app/views/Backup/Backup.tsx @@ -3,7 +3,7 @@ import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen"; import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert"; import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput"; import { TextChain } from "@/app/components/TextChain/TextChain"; -import { decodeWithPassword } from "@/app/crypto/crypto"; +import { decodeWithPassword } from "@/app/workers/crypto/crypto"; import { useAccount } from "@/app/providers/AccountProvider/useAccount"; import { Text } from "@mantine/core"; import { useState } from "react"; diff --git a/app/views/Lockscreen/Lockscreen.tsx b/app/views/Lockscreen/Lockscreen.tsx index 80697fc..c634c61 100644 --- a/app/views/Lockscreen/Lockscreen.tsx +++ b/app/views/Lockscreen/Lockscreen.tsx @@ -3,7 +3,7 @@ import classes from './Lockscreen.module.css' import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import useWindow from "@/app/hooks/useWindow"; -import { decodeWithPassword, generateHashFromPrivateKey } from "@/app/crypto/crypto"; +import { decodeWithPassword, generateHashFromPrivateKey } from "@/app/workers/crypto/crypto"; import { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider"; import { Account, AccountBase } from "@/app/providers/AccountProvider/AccountProvider"; import { useUserCache } from "@/app/providers/InformationProvider/useUserCache"; diff --git a/app/views/SetPassword/SetPassword.tsx b/app/views/SetPassword/SetPassword.tsx index 82e729a..11692ab 100644 --- a/app/views/SetPassword/SetPassword.tsx +++ b/app/views/SetPassword/SetPassword.tsx @@ -7,7 +7,7 @@ import { mnemonicToSeed } from "web-bip39"; import { useNavigate } from "react-router-dom"; import { modals } from "@mantine/modals"; import { Buffer } from 'buffer' -import { encodeWithPassword, generateHashFromPrivateKey, generateKeyPairFromSeed } from "@/app/crypto/crypto"; +import { encodeWithPassword, generateHashFromPrivateKey, generateKeyPairFromSeed } from "@/app/workers/crypto/crypto"; import { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider"; import { Account } from "@/app/providers/AccountProvider/AccountProvider"; import { useRosettaColors } from "@/app/hooks/useRosettaColors"; diff --git a/app/crypto/crypto.ts b/app/workers/crypto/crypto.ts similarity index 98% rename from app/crypto/crypto.ts rename to app/workers/crypto/crypto.ts index f7913bf..f809f28 100644 --- a/app/crypto/crypto.ts +++ b/app/workers/crypto/crypto.ts @@ -1,5 +1,5 @@ import { sha256, md5 } from "node-forge"; -import { generateRandomKey } from "../utils/utils"; +import { generateRandomKey } from "../../utils/utils"; import * as secp256k1 from '@noble/secp256k1'; const worker = new Worker(new URL('./crypto.worker.ts', import.meta.url), { type: 'module' }); diff --git a/app/crypto/crypto.worker.ts b/app/workers/crypto/crypto.worker.ts similarity index 100% rename from app/crypto/crypto.worker.ts rename to app/workers/crypto/crypto.worker.ts From 57410973347dfe8071ee0415766a9bead9d93978 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 21:53:55 +0200 Subject: [PATCH 10/15] =?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=83=D1=82=D0=B5=D1=87=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=B0=D0=BC=D1=8F=D1=82=D0=B8,=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0,=20=D0=BB=D1=83=D1=87=D1=88=D0=B0=D1=8F=20?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/workers/crypto/crypto.ts | 149 ++++++++++++---------------- app/workers/crypto/crypto.worker.ts | 72 +++++--------- 2 files changed, 88 insertions(+), 133 deletions(-) diff --git a/app/workers/crypto/crypto.ts b/app/workers/crypto/crypto.ts index f809f28..8c1714d 100644 --- a/app/workers/crypto/crypto.ts +++ b/app/workers/crypto/crypto.ts @@ -1,115 +1,88 @@ import { sha256, md5 } from "node-forge"; -import { generateRandomKey } from "../../utils/utils"; import * as secp256k1 from '@noble/secp256k1'; const worker = new Worker(new URL('./crypto.worker.ts', import.meta.url), { type: 'module' }); -export const encodeWithPassword = async (password : string, data : any) : Promise => { - let task = generateRandomKey(16); - return new Promise((resolve, _) => { - worker.addEventListener('message', (event: MessageEvent) => { - if (event.data.action === 'encodeWithPasswordResult' && event.data.task === task) { - resolve(event.data.result); - } - }); - worker.postMessage({ action: 'encodeWithPassword', data: { password, payload: data, task } }); - }); -} +type WorkerReq = + | { id: number; type: 'encodeWithPassword'; payload: { password: string; data: any } } + | { id: number; type: 'decodeWithPassword'; payload: { password: string; data: any } } + | { id: number; type: 'encrypt'; payload: { publicKey: string; data: string } } + | { id: number; type: 'decrypt'; payload: { privateKey: string; data: string } } + | { id: number; type: 'chacha20Encrypt'; payload: { data: string } } + | { id: number; type: 'chacha20Decrypt'; payload: { ciphertext: string; nonce: string; key: string } }; -export const decodeWithPassword = (password : string, data : any) : Promise => { - let task = generateRandomKey(16); +type WorkerRes = + | { id: number; ok: true; data: any } + | { id: number; ok: false; error: string }; + +let seq = 0; +const pending = new Map void>(); + +worker.onmessage = (e: MessageEvent) => { + const res = e.data; + const cb = pending.get(res.id); + if (cb) { + pending.delete(res.id); + cb(res); + } +}; + +function callWorker(req: Omit): Promise { return new Promise((resolve, reject) => { - worker.addEventListener('message', (event: MessageEvent) => { - if (event.data.action === 'decodeWithPasswordResult' && event.data.task === task) { - if(event.data.result === null){ - reject("Decryption failed"); - return; - } - resolve(event.data.result); - } - }); - worker.postMessage({ action: 'decodeWithPassword', data: { password, payload: data, task } }); + const id = ++seq; + pending.set(id, (res) => (res.ok ? resolve(res.data) : reject(res.error))); + worker.postMessage({ ...req, id }); }); } -export const generateKeyPairFromSeed = async (seed : string) => { - //generate key pair using secp256k1 includes privatekey from seed +export const encodeWithPassword = (password: string, data: any): Promise => { + return callWorker({ type: 'encodeWithPassword', payload: { password, data } }); +}; + +export const decodeWithPassword = (password: string, data: any): Promise => { + return callWorker({ type: 'decodeWithPassword', payload: { password, data } }); +}; + +export const encrypt = (data: string, publicKey: string): Promise => { + return callWorker({ type: 'encrypt', payload: { publicKey, data } }); +}; + +export const decrypt = (data: string, privateKey: string): Promise => { + return callWorker({ type: 'decrypt', payload: { privateKey, data } }); +}; + +export const chacha20Encrypt = (data: string): Promise => { + return callWorker({ type: 'chacha20Encrypt', payload: { data } }); +}; + +export const chacha20Decrypt = (ciphertext: string, nonce: string, key: string): Promise => { + return callWorker({ type: 'chacha20Decrypt', payload: { ciphertext, nonce, key } }); +}; + +export const generateKeyPairFromSeed = async (seed: string) => { const privateKey = sha256.create().update(seed).digest().toHex().toString(); const publicKey = secp256k1.getPublicKey(Buffer.from(privateKey, "hex"), true); return { privateKey: privateKey, publicKey: Buffer.from(publicKey).toString('hex'), }; +}; -} - -export const encrypt = async (data : string, publicKey : string) : Promise => { - let task = generateRandomKey(16); - return new Promise((resolve, _) => { - worker.addEventListener('message', (event: MessageEvent) => { - if (event.data.action === 'encryptResult' && event.data.task === task) { - resolve(event.data.result); - } - }); - worker.postMessage({ action: 'encrypt', data: { publicKey, payload: data, task } }); - }); -} - -export const decrypt = async (data : string, privateKey : string) : Promise => { - let task = generateRandomKey(16); - return new Promise((resolve, reject) => { - worker.addEventListener('message', (event: MessageEvent) => { - if (event.data.action === 'decryptResult' && event.data.task === task) { - if(event.data.result === null){ - reject("Decryption failed"); - return; - } - resolve(event.data.result); - } - }); - worker.postMessage({ action: 'decrypt', data: { privateKey, payload: data, task } }); - }); -} - -export const chacha20Encrypt = async (data : string) => { - let task = generateRandomKey(16); - return new Promise((resolve, _) => { - worker.addEventListener('message', (event: MessageEvent) => { - if (event.data.action === 'chacha20EncryptResult' && event.data.task === task) { - resolve(event.data.result); - } - }); - worker.postMessage({ action: 'chacha20Encrypt', data: { payload: data, task } }); - }); -} - -export const chacha20Decrypt = async (ciphertext : string, nonce : string, key : string) => { - let task = generateRandomKey(16); - return new Promise((resolve, _) => { - worker.addEventListener('message', (event: MessageEvent) => { - if (event.data.action === 'chacha20DecryptResult' && event.data.task === task) { - resolve(event.data.result); - } - }); - worker.postMessage({ action: 'chacha20Decrypt', data: { ciphertext, nonce, key, task } }); - }); -} - -export const generateMd5 = async (data : string) => { +export const generateMd5 = async (data: string) => { const hash = md5.create(); hash.update(data); return hash.digest().toHex(); -} +}; -export const generateHashFromPrivateKey = async (privateKey : string) => { +export const generateHashFromPrivateKey = async (privateKey: string) => { return sha256.create().update(privateKey + "rosetta").digest().toHex().toString(); -} +}; -export const isEncodedWithPassword = (data : string) => { - try{ +export const isEncodedWithPassword = (data: string) => { + try { atob(data).split(":"); return true; - } catch(e) { + } catch (e) { return false; } -} +}; \ No newline at end of file diff --git a/app/workers/crypto/crypto.worker.ts b/app/workers/crypto/crypto.worker.ts index ed5b8aa..884e063 100644 --- a/app/workers/crypto/crypto.worker.ts +++ b/app/workers/crypto/crypto.worker.ts @@ -6,53 +6,35 @@ import * as secp256k1 from '@noble/secp256k1'; self.onmessage = async (event: MessageEvent) => { - const { action, data } = event.data; + const { id, type, payload } = event.data; - switch (action) { - case 'encodeWithPassword': { - const { password, payload, task } = data; - const result = await encodeWithPassword(password, payload); - self.postMessage({ action: 'encodeWithPasswordResult', result, task }); - break; + try { + let result; + switch (type) { + case 'encodeWithPassword': + result = await encodeWithPassword(payload.password, payload.data); + break; + case 'decodeWithPassword': + result = await decodeWithPassword(payload.password, payload.data); + break; + case 'encrypt': + result = await encrypt(payload.publicKey, payload.data); + break; + case 'decrypt': + result = await decrypt(payload.privateKey, payload.data); + break; + case 'chacha20Encrypt': + result = await chacha20Encrypt(payload.data); + break; + case 'chacha20Decrypt': + result = await chacha20Decrypt(payload.ciphertext, payload.nonce, payload.key); + break; + default: + throw new Error(`Unknown action: ${type}`); } - case 'chacha20Encrypt': { - const { payload, task } = data; - const result = await chacha20Encrypt(payload); - self.postMessage({ action: 'chacha20EncryptResult', result, task }); - break; - } - case 'chacha20Decrypt': { - const { ciphertext, nonce, key, task } = data; - const result = await chacha20Decrypt(ciphertext, nonce, key); - self.postMessage({ action: 'chacha20DecryptResult', result, task }); - break; - } - case 'decodeWithPassword': { - const { password, payload, task } = data; - try{ - const result = await decodeWithPassword(password, payload); - self.postMessage({ action: 'decodeWithPasswordResult', result, task }); - return; - }catch(e){ - const result = null; - self.postMessage({ action: 'decodeWithPasswordResult', result, task }); - } - break; - } - case 'decrypt': { - const { payload: encryptedData, privateKey, task } = data; - const result = await decrypt(encryptedData, privateKey); - self.postMessage({ action: 'decryptResult', result, task }); - break; - } - case 'encrypt': { - const { payload: plainData, publicKey, task } = data; - const result = await encrypt(plainData, publicKey); - self.postMessage({ action: 'encryptResult', result, task }); - break; - } - default: - console.error(`Unknown action: ${action}`); + self.postMessage({ id, ok: true, data: result }); + } catch (error) { + self.postMessage({ id, ok: false, error: String(error) }); } }; From b31c757a32bdbe3eac764ace99c5a8a43981a4c8 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 21:58:02 +0200 Subject: [PATCH 11/15] =?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=B9=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B0=D1=80=D0=B3=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=BC=20=D0=B2=D0=BE?= =?UTF-8?q?=D1=80=D0=BA=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/workers/crypto/crypto.worker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/workers/crypto/crypto.worker.ts b/app/workers/crypto/crypto.worker.ts index 884e063..6765fdf 100644 --- a/app/workers/crypto/crypto.worker.ts +++ b/app/workers/crypto/crypto.worker.ts @@ -18,10 +18,11 @@ self.onmessage = async (event: MessageEvent) => { result = await decodeWithPassword(payload.password, payload.data); break; case 'encrypt': - result = await encrypt(payload.publicKey, payload.data); + result = await encrypt(payload.data, payload.publicKey); break; case 'decrypt': - result = await decrypt(payload.privateKey, payload.data); + console.info("decrypt", payload.privateKey, payload.data); + result = await decrypt(payload.data, payload.privateKey); break; case 'chacha20Encrypt': result = await chacha20Encrypt(payload.data); From d435809ae85b0ec18b28a0ad3330aa8dea120ede Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 22:35:02 +0200 Subject: [PATCH 12/15] =?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=20UI=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageAttachments/MessageImage.tsx | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/app/components/MessageAttachments/MessageImage.tsx b/app/components/MessageAttachments/MessageImage.tsx index e04ea40..77f8ef2 100644 --- a/app/components/MessageAttachments/MessageImage.tsx +++ b/app/components/MessageAttachments/MessageImage.tsx @@ -1,7 +1,7 @@ import { useRosettaColors } from "@/app/hooks/useRosettaColors"; import { DeliveredMessageState } from "@/app/providers/DialogProvider/DialogProvider"; import { useImageViewer } from "@/app/providers/ImageViewerProvider/useImageViewer"; -import { AspectRatio, Box, Flex, Overlay, Portal, Text } from "@mantine/core"; +import { AspectRatio, Box, Flex, Loader, Overlay, Portal, Text } from "@mantine/core"; import { IconArrowDown, IconCircleX, IconFlameFilled } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { AttachmentProps } from "./MessageAttachments"; @@ -111,7 +111,43 @@ export function MessageImage(props: AttachmentProps) { 95 ? 95 : uploadedPercentage}> - } + } + {props.delivered == DeliveredMessageState.WAITING && uploadedPercentage == 0 && isMessageDeliveredByTime(props.timestamp || 0, props.attachments.length) && + + + + + + Encrypting... + + + + } + {downloadStatus == DownloadStatus.DECRYPTING && + + + + + + Decrypting... + + + + } {(props.delivered == DeliveredMessageState.ERROR || (props.delivered != DeliveredMessageState.DELIVERED && !isMessageDeliveredByTime(props.timestamp || 0, props.attachments.length) )) && ( From 66a3beec2c71ff0c716e914217618edc73fbd25d Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 22:45:37 +0200 Subject: [PATCH 13/15] =?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=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=D1=8B=D1=81=D0=BE=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BA=D1=80=D0=BE=D0=BB=D0=BB=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/Messages/Messages.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/components/Messages/Messages.tsx b/app/components/Messages/Messages.tsx index 8ee6b21..e08df20 100644 --- a/app/components/Messages/Messages.tsx +++ b/app/components/Messages/Messages.tsx @@ -74,25 +74,25 @@ export function Messages() { return () => observer.disconnect(); }, [messages.length, loading]); - // MutationObserver - отслеживаем изменения контента (загрузка картинок, видео) useEffect(() => { - if (!contentRef.current) return; + if (!contentRef.current || !viewportRef.current) return; - const observer = new MutationObserver(() => { - // Скроллим только если нужен авто-скролл - if (shouldAutoScrollRef.current) { + const contentEl = contentRef.current; + //const viewportEl = viewportRef.current; + let lastHeight = contentEl.scrollHeight; + + const ro = new ResizeObserver(() => { + const newHeight = contentEl.scrollHeight; + const grew = newHeight > lastHeight; + lastHeight = newHeight; + + if (grew && shouldAutoScrollRef.current) { scrollToBottom(true); } }); - observer.observe(contentRef.current, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['src', 'style', 'class'] - }); - - return () => observer.disconnect(); + ro.observe(contentEl); + return () => ro.disconnect(); }, [scrollToBottom]); // Первый рендер - скроллим вниз моментально From c41463d88f968ae72bf2aca760767c9c6229dddf Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 22:51:52 +0200 Subject: [PATCH 14/15] =?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=20=D0=B8=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/version.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/version.ts b/app/version.ts index 68773cb..9795019 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,8 +1,15 @@ -export const APP_VERSION = "1.0.3"; +export const APP_VERSION = "1.0.4"; export const CORE_MIN_REQUIRED_VERSION = "1.4.9"; export const RELEASE_NOTICE = ` -**Update v1.0.3** :emoji_1f631: -- Fix kernel update alert -- Fix UI bugs. +**Обновление v1.0.3** :emoji_1f631: +- Улучшеный UI для взаимодействия с отправкой изображений +- Исправлена блокировка потока при отправке изображений большого размера +- Исправлены проблемы с утечками памяти +- Исправлен вылет из приложения при попытке переслать сообщение +- Исправлены проблемы со скроллам в групповых чатах +- Исправлены проблемы с дерганием скролла в личных сообщениях +- Улучшен наблюдатель за изменениями размера в контенте +- Исправлена проблема с отображением аватара в упоминаниях +- Множественные исправления мелких багов и улучшения производительности `; \ No newline at end of file From a8ec08f0f5c7d16e75711dd589169d1a16367d9a Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 19 Feb 2026 22:53:02 +0200 Subject: [PATCH 15/15] =?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 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.ts b/app/version.ts index 9795019..a1b3d59 100644 --- a/app/version.ts +++ b/app/version.ts @@ -2,7 +2,7 @@ export const APP_VERSION = "1.0.4"; export const CORE_MIN_REQUIRED_VERSION = "1.4.9"; export const RELEASE_NOTICE = ` -**Обновление v1.0.3** :emoji_1f631: +**Обновление v1.0.4** :emoji_1f631: - Улучшеный UI для взаимодействия с отправкой изображений - Исправлена блокировка потока при отправке изображений большого размера - Исправлены проблемы с утечками памяти