This commit is contained in:
@@ -59,14 +59,14 @@ jobs:
|
|||||||
chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh"
|
chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh"
|
||||||
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
||||||
-l "$GITHUB_WORKSPACE/dist/builds/darwin/x64/Rosetta-*.pkg" \
|
-l "$GITHUB_WORKSPACE/dist/builds/darwin/x64/Rosetta-*.pkg" \
|
||||||
-r "${{ secrets.SSH_TARGET_DIR }}/darwin/x64" \
|
-r "${{ secrets.SDU_SSH_TARGET_DIR }}/darwin/x64" \
|
||||||
-s "${{ secrets.SSH_HOST }}" \
|
-s "${{ secrets.SDU_SSH_HOST }}" \
|
||||||
-u "${{ secrets.SSH_USERNAME }}" \
|
-u "${{ secrets.SDU_SSH_USERNAME }}" \
|
||||||
-p '${{ secrets.SSH_PASSWORD }}'
|
-p '${{ secrets.SDU_SSH_PASSWORD }}'
|
||||||
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
||||||
-l "$GITHUB_WORKSPACE/dist/builds/darwin/arm64/Rosetta-*.pkg" \
|
-l "$GITHUB_WORKSPACE/dist/builds/darwin/arm64/Rosetta-*.pkg" \
|
||||||
-r "${{ secrets.SSH_TARGET_DIR }}/darwin/arm64" \
|
-r "${{ secrets.SDU_SSH_TARGET_DIR }}/darwin/arm64" \
|
||||||
-s "${{ secrets.SSH_HOST }}" \
|
-s "${{ secrets.SDU_SSH_HOST }}" \
|
||||||
-u "${{ secrets.SSH_USERNAME }}" \
|
-u "${{ secrets.SDU_SSH_USERNAME }}" \
|
||||||
-p '${{ secrets.SSH_PASSWORD }}'
|
-p '${{ secrets.SDU_SSH_PASSWORD }}'
|
||||||
|
|
||||||
@@ -26,9 +26,9 @@ jobs:
|
|||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.HOME }}/.npm-cache
|
path: ${{ env.HOME }}/.npm-cache
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }}
|
key: ${{ runner.os }}-npm-linux-${{ hashFiles('**/package.json') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-npm-
|
${{ runner.os }}-npm-linux-
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
# Кэш для electron-builder
|
# Кэш для electron-builder
|
||||||
- name: Cache electron-builder
|
- name: Cache electron-builder
|
||||||
@@ -37,9 +37,9 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
${{ env.HOME }}/Library/Caches/electron-builder
|
${{ env.HOME }}/Library/Caches/electron-builder
|
||||||
${{ env.HOME }}/Library/Caches/electron
|
${{ 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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-electron-builder-
|
${{ runner.os }}-electron-linux-builder-
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
- name: NPM offline setup
|
- name: NPM offline setup
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -59,14 +59,14 @@ jobs:
|
|||||||
chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh"
|
chmod +x "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh"
|
||||||
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
||||||
-l "$GITHUB_WORKSPACE/dist/builds/linux/x64/Rosetta-*.AppImage" \
|
-l "$GITHUB_WORKSPACE/dist/builds/linux/x64/Rosetta-*.AppImage" \
|
||||||
-r "${{ secrets.SSH_TARGET_DIR }}/linux/x64" \
|
-r "${{ secrets.SDU_SSH_TARGET_DIR }}/linux/x64" \
|
||||||
-s "${{ secrets.SSH_HOST }}" \
|
-s "${{ secrets.SDU_SSH_HOST }}" \
|
||||||
-u "${{ secrets.SSH_USERNAME }}" \
|
-u "${{ secrets.SDU_SSH_USERNAME }}" \
|
||||||
-p '${{ secrets.SSH_PASSWORD }}'
|
-p '${{ secrets.SDU_SSH_PASSWORD }}'
|
||||||
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
||||||
-l "$GITHUB_WORKSPACE/dist/builds/linux/arm64/Rosetta-*.AppImage" \
|
-l "$GITHUB_WORKSPACE/dist/builds/linux/arm64/Rosetta-*.AppImage" \
|
||||||
-r "${{ secrets.SSH_TARGET_DIR }}/linux/arm64" \
|
-r "${{ secrets.SDU_SSH_TARGET_DIR }}/linux/arm64" \
|
||||||
-s "${{ secrets.SSH_HOST }}" \
|
-s "${{ secrets.SDU_SSH_HOST }}" \
|
||||||
-u "${{ secrets.SSH_USERNAME }}" \
|
-u "${{ secrets.SDU_SSH_USERNAME }}" \
|
||||||
-p '${{ secrets.SSH_PASSWORD }}'
|
-p '${{ secrets.SDU_SSH_PASSWORD }}'
|
||||||
|
|
||||||
@@ -69,6 +69,6 @@ jobs:
|
|||||||
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
sh "$GITHUB_WORKSPACE/.gitea/workflows/sshupload.sh" \
|
||||||
-l "$GITHUB_WORKSPACE/packs/*" \
|
-l "$GITHUB_WORKSPACE/packs/*" \
|
||||||
-r "${{ secrets.SDU_SSH_PACKS }}" \
|
-r "${{ secrets.SDU_SSH_PACKS }}" \
|
||||||
-s "${{ secrets.SSH_HOST }}" \
|
-s "${{ secrets.SDU_SSH_HOST }}" \
|
||||||
-u "${{ secrets.SSH_USERNAME }}" \
|
-u "${{ secrets.SDU_SSH_USERNAME }}" \
|
||||||
-p '${{ secrets.SSH_PASSWORD }}'
|
-p '${{ secrets.SDU_SSH_PASSWORD }}'
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
& "$env:GITHUB_WORKSPACE\.gitea\workflows\sshupload.ps1" `
|
& "$env:GITHUB_WORKSPACE\.gitea\workflows\sshupload.ps1" `
|
||||||
-LocalFilePath "dist/builds/win/x64/Rosetta-*.exe" `
|
-LocalFilePath "dist/builds/win/x64/Rosetta-*.exe" `
|
||||||
-RemoteFolderPath "${{ secrets.SSH_TARGET_DIR }}/win32/x64" `
|
-RemoteFolderPath "${{ secrets.SDU_SSH_TARGET_DIR }}/win32/x64" `
|
||||||
-ServerAddress "${{ secrets.SSH_HOST }}" `
|
-ServerAddress "${{ secrets.SDU_SSH_HOST }}" `
|
||||||
-Username "${{ secrets.SSH_USERNAME }}" `
|
-Username "${{ secrets.SDU_SSH_USERNAME }}" `
|
||||||
-PasswordParam '${{ secrets.SSH_PASSWORD }}'
|
-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 { IconBarrierBlock, IconCamera, IconDoorExit, IconFile, IconMoodSmile, IconPaperclip, IconSend } from "@tabler/icons-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist";
|
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 { Attachment, AttachmentType } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message";
|
||||||
import { DialogAttachment } from "../DialogAttachment/DialogAttachment";
|
import { DialogAttachment } from "../DialogAttachment/DialogAttachment";
|
||||||
import { PacketTyping } from "@/app/providers/ProtocolProvider/protocol/packets/packet.typeing";
|
import { PacketTyping } from "@/app/providers/ProtocolProvider/protocol/packets/packet.typeing";
|
||||||
@@ -104,6 +104,12 @@ export function DialogInput() {
|
|||||||
}, [dialog, editableDivRef]);
|
}, [dialog, editableDivRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if(systemAccounts.find((acc) => acc.publicKey == dialog)){
|
||||||
|
/**
|
||||||
|
* У системных аккаунтов нельзя отвечать на сообщения
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(replyMessages.inDialogInput && replyMessages.inDialogInput == dialog){
|
if(replyMessages.inDialogInput && replyMessages.inDialogInput == dialog){
|
||||||
setAttachments([{
|
setAttachments([{
|
||||||
type: AttachmentType.MESSAGES,
|
type: AttachmentType.MESSAGES,
|
||||||
@@ -111,8 +117,10 @@ export function DialogInput() {
|
|||||||
blob: JSON.stringify([...replyMessages.messages]),
|
blob: JSON.stringify([...replyMessages.messages]),
|
||||||
preview: ""
|
preview: ""
|
||||||
}]);
|
}]);
|
||||||
|
if(editableDivRef.current){
|
||||||
editableDivRef.current.focus();
|
editableDivRef.current.focus();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [dialog, replyMessages]);
|
}, [dialog, replyMessages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -208,6 +216,12 @@ export function DialogInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onClickCamera = async () => {
|
const onClickCamera = async () => {
|
||||||
|
if(systemAccounts.find((acc) => acc.publicKey == dialog)){
|
||||||
|
/**
|
||||||
|
* У системных аккаунтов нельзя вызывать вложения
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(avatars.length == 0){
|
if(avatars.length == 0){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -215,10 +229,12 @@ export function DialogInput() {
|
|||||||
blob: avatars[0].avatar,
|
blob: avatars[0].avatar,
|
||||||
id: generateRandomKey(8),
|
id: generateRandomKey(8),
|
||||||
type: AttachmentType.AVATAR,
|
type: AttachmentType.AVATAR,
|
||||||
preview: await base64ImageToBlurhash(avatars[0].avatar)
|
preview: ""
|
||||||
}]);
|
}]);
|
||||||
|
if(editableDivRef.current){
|
||||||
editableDivRef.current.focus();
|
editableDivRef.current.focus();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendTypeingPacket = () => {
|
const sendTypeingPacket = () => {
|
||||||
let packet = new PacketTyping();
|
let packet = new PacketTyping();
|
||||||
@@ -229,6 +245,12 @@ export function DialogInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPaste = async (event: React.ClipboardEvent) => {
|
const onPaste = async (event: React.ClipboardEvent) => {
|
||||||
|
if(systemAccounts.find((acc) => acc.publicKey == dialog)){
|
||||||
|
/**
|
||||||
|
* У системных аккаунтов нельзя вызывать вложения
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(attachments.length >= MAX_ATTACHMENTS_IN_MESSAGE){
|
if(attachments.length >= MAX_ATTACHMENTS_IN_MESSAGE){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -242,14 +264,17 @@ export function DialogInput() {
|
|||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
const base64Image = await imagePrepareForNetworkTransfer(file);
|
const base64Image = await imagePrepareForNetworkTransfer(file);
|
||||||
|
const attachmentId = generateRandomKey(8);
|
||||||
setAttachments([...attachments, {
|
setAttachments([...attachments, {
|
||||||
blob: base64Image,
|
blob: base64Image,
|
||||||
id: generateRandomKey(8),
|
id: attachmentId,
|
||||||
type: AttachmentType.IMAGE,
|
type: AttachmentType.IMAGE,
|
||||||
preview: await base64ImageToBlurhash(base64Image)
|
preview: ""
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
if(editableDivRef.current){
|
||||||
editableDivRef.current.focus();
|
editableDivRef.current.focus();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,9 +298,10 @@ export function DialogInput() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let fileContent = await filePrapareForNetworkTransfer(file);
|
let fileContent = await filePrapareForNetworkTransfer(file);
|
||||||
|
const attachmentId = generateRandomKey(8);
|
||||||
setAttachments([...attachments, {
|
setAttachments([...attachments, {
|
||||||
blob: fileContent,
|
blob: fileContent,
|
||||||
id: generateRandomKey(8),
|
id: attachmentId,
|
||||||
type: AttachmentType.FILE,
|
type: AttachmentType.FILE,
|
||||||
preview: files[0].size + "::" + files[0].name
|
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 == 'all' && <Avatar title="@" variant="filled" color={colors.brandColor}>@</Avatar>}
|
||||||
{props.username == 'admin' && <Avatar title="@" variant="filled" color={colors.error}>@</Avatar>}
|
{props.username == 'admin' && <Avatar title="@" variant="filled" color={colors.error}>@</Avatar>}
|
||||||
{props.username != 'all' && props.username != 'admin' && <Avatar
|
{props.username != 'all' && props.username != 'admin' && <Avatar
|
||||||
title={props.title}
|
name={props.title}
|
||||||
variant="filled"
|
variant="light"
|
||||||
color="initials"
|
color="initials"
|
||||||
src={avatars.length > 0 ? avatars[0].avatar : null}
|
src={avatars.length > 0 ? avatars[0].avatar : null}
|
||||||
></Avatar>}
|
></Avatar>}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { AspectRatio, Button, Flex, Paper, Text } from "@mantine/core";
|
|||||||
import { IconArrowDown } from "@tabler/icons-react";
|
import { IconArrowDown } from "@tabler/icons-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { AttachmentProps } from "./MessageAttachments";
|
import { AttachmentProps } from "./MessageAttachments";
|
||||||
import { blurhashToBase64Image } from "@/app/utils/utils";
|
|
||||||
import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress";
|
import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress";
|
||||||
import { ImageToView } from "@/app/providers/ImageViewerProvider/ImageViewerProvider";
|
import { ImageToView } from "@/app/providers/ImageViewerProvider/ImageViewerProvider";
|
||||||
import { DownloadStatus, useAttachment } from "@/app/providers/AttachmentProvider/useAttachment";
|
import { DownloadStatus, useAttachment } from "@/app/providers/AttachmentProvider/useAttachment";
|
||||||
import { PopoverLockIconAvatar } from "../PopoverLockIconAvatar/PopoverLockIconAvatar";
|
import { PopoverLockIconAvatar } from "../PopoverLockIconAvatar/PopoverLockIconAvatar";
|
||||||
import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints";
|
import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints";
|
||||||
|
import { blurhashToBase64Image } from "@/app/workers/image/image";
|
||||||
|
|
||||||
export function MessageAvatar(props: AttachmentProps) {
|
export function MessageAvatar(props: AttachmentProps) {
|
||||||
const colors = useRosettaColors();
|
const colors = useRosettaColors();
|
||||||
@@ -25,10 +25,12 @@ export function MessageAvatar(props: AttachmentProps) {
|
|||||||
const preview = getPreview();
|
const preview = getPreview();
|
||||||
const [blob, setBlob] = useState(props.attachment.blob);
|
const [blob, setBlob] = useState(props.attachment.blob);
|
||||||
const {lg} = useRosettaBreakpoints();
|
const {lg} = useRosettaBreakpoints();
|
||||||
|
const [blurhashPreview, setBlurhashPreview] = useState("");
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
constructBlob();
|
constructBlob();
|
||||||
|
constructFromBlurhash();
|
||||||
}, [downloadStatus]);
|
}, [downloadStatus]);
|
||||||
|
|
||||||
const constructBlob = async () => {
|
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 (
|
return (
|
||||||
<Paper withBorder p={'sm'}>
|
<Paper withBorder p={'sm'}>
|
||||||
<Flex gap={'sm'} direction={'row'}>
|
<Flex gap={'sm'} direction={'row'}>
|
||||||
@@ -79,7 +87,7 @@ export function MessageAvatar(props: AttachmentProps) {
|
|||||||
height: 60,
|
height: 60,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
objectFit: 'cover'
|
objectFit: 'cover'
|
||||||
}} src={/*block render???*/blurhashToBase64Image(preview, 200, 220)}></img>
|
}} src={blurhashPreview == "" ? undefined : blurhashPreview}></img>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</AspectRatio>
|
</AspectRatio>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
||||||
import { DeliveredMessageState } from "@/app/providers/DialogProvider/DialogProvider";
|
import { DeliveredMessageState } from "@/app/providers/DialogProvider/DialogProvider";
|
||||||
import { useImageViewer } from "@/app/providers/ImageViewerProvider/useImageViewer";
|
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 { IconArrowDown, IconCircleX, IconFlameFilled } from "@tabler/icons-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { AttachmentProps } from "./MessageAttachments";
|
import { AttachmentProps } from "./MessageAttachments";
|
||||||
import { blurhashToBase64Image, isMessageDeliveredByTime } from "@/app/utils/utils";
|
import { isMessageDeliveredByTime } from "@/app/utils/utils";
|
||||||
import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress";
|
import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress";
|
||||||
import { ImageToView } from "@/app/providers/ImageViewerProvider/ImageViewerProvider";
|
import { ImageToView } from "@/app/providers/ImageViewerProvider/ImageViewerProvider";
|
||||||
import { DownloadStatus, useAttachment } from "@/app/providers/AttachmentProvider/useAttachment";
|
import { DownloadStatus, useAttachment } from "@/app/providers/AttachmentProvider/useAttachment";
|
||||||
|
import { blurhashToBase64Image } from "@/app/workers/image/image";
|
||||||
|
|
||||||
export function MessageImage(props: AttachmentProps) {
|
export function MessageImage(props: AttachmentProps) {
|
||||||
const colors = useRosettaColors();
|
const colors = useRosettaColors();
|
||||||
@@ -25,9 +26,11 @@ export function MessageImage(props: AttachmentProps) {
|
|||||||
const preview = getPreview();
|
const preview = getPreview();
|
||||||
const [blob, setBlob] = useState(props.attachment.blob);
|
const [blob, setBlob] = useState(props.attachment.blob);
|
||||||
const [loadedImage, setLoadedImage] = useState(false);
|
const [loadedImage, setLoadedImage] = useState(false);
|
||||||
|
const [blurhashPreview, setBlurhashPreview] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
constructBlob();
|
constructBlob();
|
||||||
|
constructFromBlurhash();
|
||||||
}, [downloadStatus]);
|
}, [downloadStatus]);
|
||||||
|
|
||||||
const constructBlob = async () => {
|
const constructBlob = async () => {
|
||||||
@@ -45,6 +48,12 @@ export function MessageImage(props: AttachmentProps) {
|
|||||||
open(images, 0);
|
open(images, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const constructFromBlurhash = async () => {
|
||||||
|
if (preview.length < 20) return;
|
||||||
|
const blob = await blurhashToBase64Image(preview, 200, 220);
|
||||||
|
setBlurhashPreview(blob);
|
||||||
|
}
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (downloadStatus == DownloadStatus.DOWNLOADED) {
|
if (downloadStatus == DownloadStatus.DOWNLOADED) {
|
||||||
openImageViewer();
|
openImageViewer();
|
||||||
@@ -83,7 +92,7 @@ export function MessageImage(props: AttachmentProps) {
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
border: '1px solid ' + colors.borderColor
|
border: '1px solid ' + colors.borderColor
|
||||||
}} src={/*block render???*/blurhashToBase64Image(preview, 200, 220)}></img>
|
}} src={blurhashPreview == "" ? undefined : blurhashPreview}></img>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -103,6 +112,42 @@ export function MessageImage(props: AttachmentProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</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 &&
|
{(props.delivered == DeliveredMessageState.ERROR || (props.delivered != DeliveredMessageState.DELIVERED &&
|
||||||
!isMessageDeliveredByTime(props.timestamp || 0, props.attachments.length)
|
!isMessageDeliveredByTime(props.timestamp || 0, props.attachments.length)
|
||||||
)) && (
|
)) && (
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ import { MessageSkeleton } from "../MessageSkeleton/MessageSkeleton";
|
|||||||
import { ScrollArea } from "@mantine/core";
|
import { ScrollArea } from "@mantine/core";
|
||||||
import { MESSAGE_AVATAR_NO_RENDER_TIME_DIFF_S, SCROLL_TOP_IN_MESSAGES_TO_VIEW_AFFIX } from "@/app/constants";
|
import { MESSAGE_AVATAR_NO_RENDER_TIME_DIFF_S, SCROLL_TOP_IN_MESSAGES_TO_VIEW_AFFIX } from "@/app/constants";
|
||||||
import { DialogAffix } from "../DialogAffix/DialogAffix";
|
import { DialogAffix } from "../DialogAffix/DialogAffix";
|
||||||
import { useReplyMessages } from "@/app/providers/DialogProvider/useReplyMessages";
|
|
||||||
import { useSetting } from "@/app/providers/SettingsProvider/useSetting";
|
import { useSetting } from "@/app/providers/SettingsProvider/useSetting";
|
||||||
|
|
||||||
export function Messages() {
|
export function Messages() {
|
||||||
const colors = useRosettaColors();
|
const colors = useRosettaColors();
|
||||||
const publicKey = usePublicKey();
|
const publicKey = usePublicKey();
|
||||||
const { messages, dialog, loadMessagesToTop, loading } = useDialog();
|
const { messages, dialog, loadMessagesToTop, loading } = useDialog();
|
||||||
const { replyMessages, isSelectionStarted } = useReplyMessages();
|
|
||||||
|
|
||||||
const viewportRef = useRef<HTMLDivElement | null>(null);
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||||
const lastMessageRef = useRef<HTMLDivElement | null>(null);
|
const lastMessageRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -22,6 +20,7 @@ export function Messages() {
|
|||||||
const shouldAutoScrollRef = useRef(true);
|
const shouldAutoScrollRef = useRef(true);
|
||||||
const isFirstRenderRef = useRef(true);
|
const isFirstRenderRef = useRef(true);
|
||||||
const previousScrollHeightRef = useRef(0);
|
const previousScrollHeightRef = useRef(0);
|
||||||
|
const distanceFromButtomRef = useRef(0);
|
||||||
|
|
||||||
const [affix, setAffix] = useState(false);
|
const [affix, setAffix] = useState(false);
|
||||||
const [wallpaper] = useSetting<string>
|
const [wallpaper] = useSetting<string>
|
||||||
@@ -75,25 +74,25 @@ export function Messages() {
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [messages.length, loading]);
|
}, [messages.length, loading]);
|
||||||
|
|
||||||
// MutationObserver - отслеживаем изменения контента (загрузка картинок, видео)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!contentRef.current) return;
|
if (!contentRef.current || !viewportRef.current) return;
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const contentEl = contentRef.current;
|
||||||
// Скроллим только если нужен авто-скролл
|
//const viewportEl = viewportRef.current;
|
||||||
if (shouldAutoScrollRef.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);
|
scrollToBottom(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(contentRef.current, {
|
ro.observe(contentEl);
|
||||||
childList: true,
|
return () => ro.disconnect();
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['src', 'style', 'class']
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [scrollToBottom]);
|
}, [scrollToBottom]);
|
||||||
|
|
||||||
// Первый рендер - скроллим вниз моментально
|
// Первый рендер - скроллим вниз моментально
|
||||||
@@ -121,15 +120,10 @@ export function Messages() {
|
|||||||
const lastMessage = messages[messages.length - 1];
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
|
||||||
// Скроллим если пользователь внизу или это его собственное сообщение
|
// Скроллим если пользователь внизу или это его собственное сообщение
|
||||||
if ((shouldAutoScrollRef.current || lastMessage.from_me) && !affix) {
|
if ((shouldAutoScrollRef.current || lastMessage.from_me)) {
|
||||||
/**
|
|
||||||
* Скролл только если пользователь не читает сейчас старую переписку
|
|
||||||
* (!affix))
|
|
||||||
*/
|
|
||||||
//console.info("Scroll because", shouldAutoScrollRef.current);
|
|
||||||
scrollToBottom(true);
|
scrollToBottom(true);
|
||||||
}
|
}
|
||||||
}, [messages.length, loading, affix, scrollToBottom]);
|
}, [messages.length, loading, scrollToBottom]);
|
||||||
|
|
||||||
// Восстановление позиции после загрузки старых сообщений
|
// Восстановление позиции после загрузки старых сообщений
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -142,12 +136,6 @@ export function Messages() {
|
|||||||
}
|
}
|
||||||
}, [messages.length]);
|
}, [messages.length]);
|
||||||
|
|
||||||
// Скролл при отправке reply сообщения
|
|
||||||
useEffect(() => {
|
|
||||||
if (replyMessages.messages.length === 0 || isSelectionStarted()) return;
|
|
||||||
scrollToBottom(true);
|
|
||||||
}, [replyMessages.messages.length]);
|
|
||||||
|
|
||||||
const loadMessagesToScrollAreaTop = async () => {
|
const loadMessagesToScrollAreaTop = async () => {
|
||||||
if (!viewportRef.current) return;
|
if (!viewportRef.current) return;
|
||||||
|
|
||||||
@@ -195,6 +183,7 @@ export function Messages() {
|
|||||||
// Показываем/скрываем кнопку "вниз"
|
// Показываем/скрываем кнопку "вниз"
|
||||||
const distanceFromBottom =
|
const distanceFromBottom =
|
||||||
(viewportRef.current.scrollHeight - viewportRef.current.clientHeight) - scroll.y;
|
(viewportRef.current.scrollHeight - viewportRef.current.clientHeight) - scroll.y;
|
||||||
|
distanceFromButtomRef.current = distanceFromBottom;
|
||||||
|
|
||||||
setAffix(distanceFromBottom > SCROLL_TOP_IN_MESSAGES_TO_VIEW_AFFIX);
|
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 { useFileStorage } from "../../hooks/useFileStorage";
|
||||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||||
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
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 { useTransport } from "../TransportProvider/useTransport";
|
||||||
import { useDialogsCache } from "../DialogProvider/useDialogsCache";
|
import { useDialogsCache } from "../DialogProvider/useDialogsCache";
|
||||||
import { useConsoleLogger } from "../../hooks/useConsoleLogger";
|
import { useConsoleLogger } from "../../hooks/useConsoleLogger";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
|||||||
import { createContext, useEffect, useRef, useState } from "react";
|
import { createContext, useEffect, useRef, useState } from "react";
|
||||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||||
import { useFileStorage } from "@/app/hooks/useFileStorage";
|
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 { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
|
||||||
import { useSystemAccounts } from "../SystemAccountsProvider/useSystemAccounts";
|
import { useSystemAccounts } from "../SystemAccountsProvider/useSystemAccounts";
|
||||||
import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants";
|
import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { DialogRow } from "./DialogListProvider";
|
import { DialogRow } from "./DialogListProvider";
|
||||||
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
||||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
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 { constructLastMessageTextByAttachments } from "@/app/utils/constructLastMessageTextByAttachments";
|
||||||
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
||||||
import { DeliveredMessageState, Message } from "../DialogProvider/DialogProvider";
|
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 { useDatabase } from '@/app/providers/DatabaseProvider/useDatabase';
|
||||||
import { createContext, useEffect, useRef, useState } from 'react';
|
import { createContext, useEffect, useRef, useState } from 'react';
|
||||||
import { Attachment, AttachmentType, PacketMessage } from '@/app/providers/ProtocolProvider/protocol/packets/packet.message';
|
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 { useDialogsList } from '../DialogListProvider/useDialogsList';
|
||||||
import { useGroups } from './useGroups';
|
import { useGroups } from './useGroups';
|
||||||
import { useMentions } from '../DialogStateProvider.tsx/useMentions';
|
import { useMentions } from '../DialogStateProvider.tsx/useMentions';
|
||||||
|
import { base64ImageToBlurhash } from '@/app/workers/image/image';
|
||||||
|
|
||||||
export interface DialogContextValue {
|
export interface DialogContextValue {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -923,6 +924,14 @@ export function DialogProvider(props: DialogProviderProps) {
|
|||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if((attachment.type == AttachmentType.IMAGE
|
||||||
|
|| attachment.type == AttachmentType.AVATAR) && attachment.preview == ""){
|
||||||
|
/**
|
||||||
|
* Загружаем превью blurhash для изображения
|
||||||
|
*/
|
||||||
|
const blurhash = await base64ImageToBlurhash(attachment.blob);
|
||||||
|
attachment.preview = blurhash;
|
||||||
|
}
|
||||||
doTimestampUpdateImMessageWhileAttachmentsSend(attachments);
|
doTimestampUpdateImMessageWhileAttachmentsSend(attachments);
|
||||||
const content = await encodeWithPassword(password, attachment.blob);
|
const content = await encodeWithPassword(password, attachment.blob);
|
||||||
const upid = attachment.id;
|
const upid = attachment.id;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { useDatabase } from "../DatabaseProvider/useDatabase";
|
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 { AttachmentMeta, DeliveredMessageState, DialogContext, Message } from "./DialogProvider";
|
||||||
import { Attachment, AttachmentType, PacketMessage } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message";
|
import { Attachment, AttachmentType, PacketMessage } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message";
|
||||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useDialogsCache } from "./useDialogsCache";
|
|||||||
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
||||||
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
||||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
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 { DeliveredMessageState, Message } from "./DialogProvider";
|
||||||
import { PacketRead } from "../ProtocolProvider/protocol/packets/packet.read";
|
import { PacketRead } from "../ProtocolProvider/protocol/packets/packet.read";
|
||||||
import { PacketDelivery } from "../ProtocolProvider/protocol/packets/packet.delivery";
|
import { PacketDelivery } from "../ProtocolProvider/protocol/packets/packet.delivery";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
|
||||||
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
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 { generateRandomKey } from "@/app/utils/utils";
|
||||||
import { useDialogsList } from "../DialogListProvider/useDialogsList";
|
import { useDialogsList } from "../DialogListProvider/useDialogsList";
|
||||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createContext } from "react";
|
|||||||
import { useSystemAccount } from "./useSystemAccount";
|
import { useSystemAccount } from "./useSystemAccount";
|
||||||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||||
import { usePrivatePlain } from "../AccountProvider/usePrivatePlain";
|
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 { generateRandomKey } from "@/app/utils/utils";
|
||||||
import { DeliveredMessageState } from "../DialogProvider/DialogProvider";
|
import { DeliveredMessageState } from "../DialogProvider/DialogProvider";
|
||||||
import { UserInformation } from "../InformationProvider/InformationProvider";
|
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 { useFileStorage } from "@/app/hooks/useFileStorage";
|
||||||
import { generateRandomKey } from "@/app/utils/utils";
|
import { generateRandomKey } from "@/app/utils/utils";
|
||||||
import { createContext, useEffect, useState } from "react";
|
import { createContext, useEffect, useState } from "react";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { MantineColor } from "@mantine/core";
|
import { MantineColor } from "@mantine/core";
|
||||||
import { MESSAGE_MAX_TIME_TO_DELEVERED_S } from "../constants";
|
import { MESSAGE_MAX_TIME_TO_DELEVERED_S } from "../constants";
|
||||||
import { decode, encode } from "blurhash";
|
|
||||||
|
|
||||||
export function generateRandomKey(length: number): string {
|
export function generateRandomKey(length: number): string {
|
||||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
@@ -243,47 +242,3 @@ export function isImage(blob : string) : boolean {
|
|||||||
}
|
}
|
||||||
return blob.startsWith('data:image/');
|
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 CORE_MIN_REQUIRED_VERSION = "1.4.9";
|
||||||
|
|
||||||
export const RELEASE_NOTICE = `
|
export const RELEASE_NOTICE = `
|
||||||
**Update v1.0.3** :emoji_1f631:
|
**Обновление v1.0.4** :emoji_1f631:
|
||||||
- Fix kernel update alert
|
- Улучшеный UI для взаимодействия с отправкой изображений
|
||||||
- Fix UI bugs.
|
- Исправлена блокировка потока при отправке изображений большого размера
|
||||||
|
- Исправлены проблемы с утечками памяти
|
||||||
|
- Исправлен вылет из приложения при попытке переслать сообщение
|
||||||
|
- Исправлены проблемы со скроллам в групповых чатах
|
||||||
|
- Исправлены проблемы с дерганием скролла в личных сообщениях
|
||||||
|
- Улучшен наблюдатель за изменениями размера в контенте
|
||||||
|
- Исправлена проблема с отображением аватара в упоминаниях
|
||||||
|
- Множественные исправления мелких багов и улучшения производительности
|
||||||
`;
|
`;
|
||||||
@@ -3,7 +3,7 @@ import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
|
|||||||
import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert";
|
import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert";
|
||||||
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
|
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
|
||||||
import { TextChain } from "@/app/components/TextChain/TextChain";
|
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 { useAccount } from "@/app/providers/AccountProvider/useAccount";
|
||||||
import { Text } from "@mantine/core";
|
import { Text } from "@mantine/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import classes from './Lockscreen.module.css'
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import useWindow from "@/app/hooks/useWindow";
|
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 { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider";
|
||||||
import { Account, AccountBase } from "@/app/providers/AccountProvider/AccountProvider";
|
import { Account, AccountBase } from "@/app/providers/AccountProvider/AccountProvider";
|
||||||
import { useUserCache } from "@/app/providers/InformationProvider/useUserCache";
|
import { useUserCache } from "@/app/providers/InformationProvider/useUserCache";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { mnemonicToSeed } from "web-bip39";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { Buffer } from 'buffer'
|
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 { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider";
|
||||||
import { Account } from "@/app/providers/AccountProvider/AccountProvider";
|
import { Account } from "@/app/providers/AccountProvider/AccountProvider";
|
||||||
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
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) => {
|
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;
|
|
||||||
}
|
|
||||||
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 {
|
try {
|
||||||
const result = await decodeWithPassword(password, payload);
|
let result;
|
||||||
self.postMessage({ action: 'decodeWithPasswordResult', result, task });
|
switch (type) {
|
||||||
return;
|
case 'encodeWithPassword':
|
||||||
}catch(e){
|
result = await encodeWithPassword(payload.password, payload.data);
|
||||||
const result = null;
|
|
||||||
self.postMessage({ action: 'decodeWithPasswordResult', result, task });
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
case 'decodeWithPassword':
|
||||||
case 'decrypt': {
|
result = await decodeWithPassword(payload.password, payload.data);
|
||||||
const { payload: encryptedData, privateKey, task } = data;
|
|
||||||
const result = await decrypt(encryptedData, privateKey);
|
|
||||||
self.postMessage({ action: 'decryptResult', result, task });
|
|
||||||
break;
|
break;
|
||||||
}
|
case 'encrypt':
|
||||||
case 'encrypt': {
|
result = await encrypt(payload.data, payload.publicKey);
|
||||||
const { payload: plainData, publicKey, task } = data;
|
break;
|
||||||
const result = await encrypt(plainData, publicKey);
|
case 'decrypt':
|
||||||
self.postMessage({ action: 'encryptResult', result, task });
|
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;
|
break;
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown action: ${action}`);
|
throw new Error(`Unknown action: ${type}`);
|
||||||
|
}
|
||||||
|
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