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 ed1951a..f57760d 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 @@ -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 ab5bf4a..9ce0ca3 100644 --- a/.gitea/workflows/service-packs.yaml +++ b/.gitea/workflows/service-packs.yaml @@ -7,7 +7,7 @@ on: branches: - main paths: - - 'app/**' + - 'app/**' jobs: build: @@ -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 }}' diff --git a/app/components/DialogInput/DialogInput.tsx b/app/components/DialogInput/DialogInput.tsx index 4f3977c..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"; @@ -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; } @@ -215,9 +229,11 @@ export function DialogInput() { blob: avatars[0].avatar, id: generateRandomKey(8), type: AttachmentType.AVATAR, - preview: await base64ImageToBlurhash(avatars[0].avatar) + preview: "" }]); - editableDivRef.current.focus(); + if(editableDivRef.current){ + editableDivRef.current.focus(); + } } const sendTypeingPacket = () => { @@ -229,6 +245,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; } @@ -242,14 +264,17 @@ 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: "" }]); } - editableDivRef.current.focus(); + if(editableDivRef.current){ + editableDivRef.current.focus(); + } break; } } @@ -273,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/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} >} 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..77f8ef2 100644 --- a/app/components/MessageAttachments/MessageImage.tsx +++ b/app/components/MessageAttachments/MessageImage.tsx @@ -1,14 +1,15 @@ 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"; -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}> )} @@ -102,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) )) && ( diff --git a/app/components/Messages/Messages.tsx b/app/components/Messages/Messages.tsx index 44b1a6c..e08df20 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 @@ -75,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]); // Первый рендер - скроллим вниз моментально @@ -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); }} diff --git a/app/crypto/crypto.ts b/app/crypto/crypto.ts deleted file mode 100644 index f7913bf..0000000 --- a/app/crypto/crypto.ts +++ /dev/null @@ -1,115 +0,0 @@ -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 } }); - }); -} - -export const decodeWithPassword = (password : string, data : any) : Promise => { - let task = generateRandomKey(16); - 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 } }); - }); -} - -export const generateKeyPairFromSeed = async (seed : string) => { - //generate key pair using secp256k1 includes privatekey from seed - 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) => { - const hash = md5.create(); - hash.update(data); - return hash.digest().toHex(); -} - -export const generateHashFromPrivateKey = async (privateKey : string) => { - return sha256.create().update(privateKey + "rosetta").digest().toHex().toString(); -} - -export const isEncodedWithPassword = (data : string) => { - try{ - atob(data).split(":"); - return true; - } catch(e) { - return false; - } -} 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 448edf4..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'; @@ -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/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/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/version.ts b/app/version.ts index 68773cb..a1b3d59 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.4** :emoji_1f631: +- Улучшеный UI для взаимодействия с отправкой изображений +- Исправлена блокировка потока при отправке изображений большого размера +- Исправлены проблемы с утечками памяти +- Исправлен вылет из приложения при попытке переслать сообщение +- Исправлены проблемы со скроллам в групповых чатах +- Исправлены проблемы с дерганием скролла в личных сообщениях +- Улучшен наблюдатель за изменениями размера в контенте +- Исправлена проблема с отображением аватара в упоминаниях +- Множественные исправления мелких багов и улучшения производительности `; \ No newline at end of file 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/workers/crypto/crypto.ts b/app/workers/crypto/crypto.ts new file mode 100644 index 0000000..8c1714d --- /dev/null +++ b/app/workers/crypto/crypto.ts @@ -0,0 +1,88 @@ +import { sha256, md5 } from "node-forge"; +import * as secp256k1 from '@noble/secp256k1'; + +const worker = new Worker(new URL('./crypto.worker.ts', import.meta.url), { type: 'module' }); + +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 } }; + +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) => { + const id = ++seq; + pending.set(id, (res) => (res.ok ? resolve(res.data) : reject(res.error))); + worker.postMessage({ ...req, id }); + }); +} + +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 generateMd5 = async (data: string) => { + const hash = md5.create(); + hash.update(data); + return hash.digest().toHex(); +}; + +export const generateHashFromPrivateKey = async (privateKey: string) => { + return sha256.create().update(privateKey + "rosetta").digest().toHex().toString(); +}; + +export const isEncodedWithPassword = (data: string) => { + try { + atob(data).split(":"); + return true; + } catch (e) { + return false; + } +}; \ No newline at end of file diff --git a/app/crypto/crypto.worker.ts b/app/workers/crypto/crypto.worker.ts similarity index 83% rename from app/crypto/crypto.worker.ts rename to app/workers/crypto/crypto.worker.ts index ed5b8aa..6765fdf 100644 --- a/app/crypto/crypto.worker.ts +++ b/app/workers/crypto/crypto.worker.ts @@ -6,53 +6,36 @@ 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.data, payload.publicKey); + break; + case 'decrypt': + console.info("decrypt", payload.privateKey, payload.data); + result = await decrypt(payload.data, payload.privateKey); + 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) }); } }; 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