Compare commits
16 Commits
f8ca15422f
...
9c8d3865a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8d3865a6 | ||
|
|
a8ec08f0f5 | ||
|
|
c41463d88f | ||
|
|
66a3beec2c | ||
|
|
d435809ae8 | ||
|
|
b31c757a32 | ||
|
|
5741097334 | ||
|
|
95a1f57381 | ||
|
|
53535d68e0 | ||
|
|
a38a331cd1 | ||
|
|
6908dd486c | ||
|
|
026a3c9520 | ||
|
|
a0c73c807f | ||
|
|
ff96dfd204 | ||
|
|
f959c6335c | ||
|
|
bb55fb47aa |
@@ -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 }}'
|
||||
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -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 }}'
|
||||
-s "${{ secrets.SDU_SSH_HOST }}" \
|
||||
-u "${{ secrets.SDU_SSH_USERNAME }}" \
|
||||
-p '${{ secrets.SDU_SSH_PASSWORD }}'
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -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
|
||||
}]);
|
||||
|
||||
@@ -19,8 +19,8 @@ export function MentionRow(props : MentionRowProps) {
|
||||
{props.username == 'all' && <Avatar title="@" variant="filled" color={colors.brandColor}>@</Avatar>}
|
||||
{props.username == 'admin' && <Avatar title="@" variant="filled" color={colors.error}>@</Avatar>}
|
||||
{props.username != 'all' && props.username != 'admin' && <Avatar
|
||||
title={props.title}
|
||||
variant="filled"
|
||||
name={props.title}
|
||||
variant="light"
|
||||
color="initials"
|
||||
src={avatars.length > 0 ? avatars[0].avatar : null}
|
||||
></Avatar>}
|
||||
|
||||
@@ -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 (
|
||||
<Paper withBorder p={'sm'}>
|
||||
<Flex gap={'sm'} direction={'row'}>
|
||||
@@ -79,7 +87,7 @@ export function MessageAvatar(props: AttachmentProps) {
|
||||
height: 60,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover'
|
||||
}} src={/*block render???*/blurhashToBase64Image(preview, 200, 220)}></img>
|
||||
}} src={blurhashPreview == "" ? undefined : blurhashPreview}></img>
|
||||
</>
|
||||
)}
|
||||
</AspectRatio>
|
||||
|
||||
@@ -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)}></img>
|
||||
}} src={blurhashPreview == "" ? undefined : blurhashPreview}></img>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -102,7 +111,43 @@ export function MessageImage(props: AttachmentProps) {
|
||||
<AnimatedRoundedProgress size={40} value={uploadedPercentage > 95 ? 95 : uploadedPercentage}></AnimatedRoundedProgress>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Portal>}
|
||||
</Portal>}
|
||||
{props.delivered == DeliveredMessageState.WAITING && uploadedPercentage == 0 && isMessageDeliveredByTime(props.timestamp || 0, props.attachments.length) &&
|
||||
<Portal target={mainRef.current!}>
|
||||
<Flex direction={'column'} pos={'absolute'} justify={'center'} top={0} h={'100%'} align={'center'} gap={'xs'}>
|
||||
<Box p={'xs'} style={{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}>
|
||||
<Loader size={15} type={'dots'} color={'white'}></Loader>
|
||||
<Text size={'xs'} c={'white'}>
|
||||
Encrypting...
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Portal>}
|
||||
{downloadStatus == DownloadStatus.DECRYPTING &&
|
||||
<Portal target={mainRef.current!}>
|
||||
<Flex direction={'column'} pos={'absolute'} justify={'center'} top={0} h={'100%'} align={'center'} gap={'xs'}>
|
||||
<Box p={'xs'} style={{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}>
|
||||
<Loader size={15} type={'dots'} color={'white'}></Loader>
|
||||
<Text size={'xs'} c={'white'}>
|
||||
Decrypting...
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Portal>}
|
||||
{(props.delivered == DeliveredMessageState.ERROR || (props.delivered != DeliveredMessageState.DELIVERED &&
|
||||
!isMessageDeliveredByTime(props.timestamp || 0, props.attachments.length)
|
||||
)) && (
|
||||
|
||||
@@ -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<HTMLDivElement | null>(null);
|
||||
const lastMessageRef = useRef<HTMLDivElement | null>(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<string>
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string> {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
return new Promise<string>((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;
|
||||
});
|
||||
}
|
||||
@@ -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 для взаимодействия с отправкой изображений
|
||||
- Исправлена блокировка потока при отправке изображений большого размера
|
||||
- Исправлены проблемы с утечками памяти
|
||||
- Исправлен вылет из приложения при попытке переслать сообщение
|
||||
- Исправлены проблемы со скроллам в групповых чатах
|
||||
- Исправлены проблемы с дерганием скролла в личных сообщениях
|
||||
- Улучшен наблюдатель за изменениями размера в контенте
|
||||
- Исправлена проблема с отображением аватара в упоминаниях
|
||||
- Множественные исправления мелких багов и улучшения производительности
|
||||
`;
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
88
app/workers/crypto/crypto.ts
Normal file
88
app/workers/crypto/crypto.ts
Normal file
@@ -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<number, (res: WorkerRes) => void>();
|
||||
|
||||
worker.onmessage = (e: MessageEvent<WorkerRes>) => {
|
||||
const res = e.data;
|
||||
const cb = pending.get(res.id);
|
||||
if (cb) {
|
||||
pending.delete(res.id);
|
||||
cb(res);
|
||||
}
|
||||
};
|
||||
|
||||
function callWorker(req: Omit<WorkerReq, 'id'>): Promise<any> {
|
||||
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<any> => {
|
||||
return callWorker({ type: 'encodeWithPassword', payload: { password, data } });
|
||||
};
|
||||
|
||||
export const decodeWithPassword = (password: string, data: any): Promise<any> => {
|
||||
return callWorker({ type: 'decodeWithPassword', payload: { password, data } });
|
||||
};
|
||||
|
||||
export const encrypt = (data: string, publicKey: string): Promise<any> => {
|
||||
return callWorker({ type: 'encrypt', payload: { publicKey, data } });
|
||||
};
|
||||
|
||||
export const decrypt = (data: string, privateKey: string): Promise<any> => {
|
||||
return callWorker({ type: 'decrypt', payload: { privateKey, data } });
|
||||
};
|
||||
|
||||
export const chacha20Encrypt = (data: string): Promise<any> => {
|
||||
return callWorker({ type: 'chacha20Encrypt', payload: { data } });
|
||||
};
|
||||
|
||||
export const chacha20Decrypt = (ciphertext: string, nonce: string, key: string): Promise<any> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -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) });
|
||||
}
|
||||
};
|
||||
|
||||
39
app/workers/image/image.ts
Normal file
39
app/workers/image/image.ts
Normal file
@@ -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<number, (res: WorkerRes) => void>();
|
||||
|
||||
worker.onmessage = (e: MessageEvent<WorkerRes>) => {
|
||||
const res = e.data;
|
||||
const cb = pending.get(res.id);
|
||||
if (cb) {
|
||||
pending.delete(res.id);
|
||||
cb(res);
|
||||
}
|
||||
};
|
||||
|
||||
function callWorker(req: Omit<WorkerReq, 'id'>): Promise<string> {
|
||||
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<string> {
|
||||
return callWorker({ type: 'blurhashToBase64Image', payload: { blurhash, width, height } });
|
||||
}
|
||||
|
||||
export function base64ImageToBlurhash(base64Image: string): Promise<string> {
|
||||
return callWorker({ type: 'base64ImageToBlurhash', payload: { base64Image } });
|
||||
}
|
||||
// ...existing code...
|
||||
71
app/workers/image/image.worker.ts
Normal file
71
app/workers/image/image.worker.ts
Normal file
@@ -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<string> => {
|
||||
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<string> => {
|
||||
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<Req>) => {
|
||||
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) });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user