Исправлена блокировка потока при вставке изображений, оптимизирован код и ответственность
This commit is contained in:
@@ -4,7 +4,7 @@ import { Box, Divider, Flex, Menu, Popover, Text, Transition, useComputedColorSc
|
||||
import { IconBarrierBlock, IconCamera, IconDoorExit, IconFile, IconMoodSmile, IconPaperclip, IconSend } from "@tabler/icons-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist";
|
||||
import { base64ImageToBlurhash, filePrapareForNetworkTransfer, generateRandomKey, imagePrepareForNetworkTransfer } from "@/app/utils/utils";
|
||||
import { filePrapareForNetworkTransfer, generateRandomKey, imagePrepareForNetworkTransfer } from "@/app/utils/utils";
|
||||
import { Attachment, AttachmentType } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message";
|
||||
import { DialogAttachment } from "../DialogAttachment/DialogAttachment";
|
||||
import { PacketTyping } from "@/app/providers/ProtocolProvider/protocol/packets/packet.typeing";
|
||||
@@ -229,7 +229,7 @@ export function DialogInput() {
|
||||
blob: avatars[0].avatar,
|
||||
id: generateRandomKey(8),
|
||||
type: AttachmentType.AVATAR,
|
||||
preview: await base64ImageToBlurhash(avatars[0].avatar)
|
||||
preview: ""
|
||||
}]);
|
||||
if(editableDivRef.current){
|
||||
editableDivRef.current.focus();
|
||||
@@ -264,11 +264,12 @@ export function DialogInput() {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
const base64Image = await imagePrepareForNetworkTransfer(file);
|
||||
const attachmentId = generateRandomKey(8);
|
||||
setAttachments([...attachments, {
|
||||
blob: base64Image,
|
||||
id: generateRandomKey(8),
|
||||
id: attachmentId,
|
||||
type: AttachmentType.IMAGE,
|
||||
preview: await base64ImageToBlurhash(base64Image)
|
||||
preview: ""
|
||||
}]);
|
||||
}
|
||||
if(editableDivRef.current){
|
||||
@@ -297,9 +298,10 @@ export function DialogInput() {
|
||||
return;
|
||||
}
|
||||
let fileContent = await filePrapareForNetworkTransfer(file);
|
||||
const attachmentId = generateRandomKey(8);
|
||||
setAttachments([...attachments, {
|
||||
blob: fileContent,
|
||||
id: generateRandomKey(8),
|
||||
id: attachmentId,
|
||||
type: AttachmentType.FILE,
|
||||
preview: files[0].size + "::" + files[0].name
|
||||
}]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,10 +5,11 @@ import { AspectRatio, Box, Flex, Overlay, Portal, Text } from "@mantine/core";
|
||||
import { IconArrowDown, IconCircleX, IconFlameFilled } from "@tabler/icons-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AttachmentProps } from "./MessageAttachments";
|
||||
import { blurhashToBase64Image, isMessageDeliveredByTime } from "@/app/utils/utils";
|
||||
import { isMessageDeliveredByTime } from "@/app/utils/utils";
|
||||
import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress";
|
||||
import { ImageToView } from "@/app/providers/ImageViewerProvider/ImageViewerProvider";
|
||||
import { DownloadStatus, useAttachment } from "@/app/providers/AttachmentProvider/useAttachment";
|
||||
import { blurhashToBase64Image } from "@/app/workers/image/image";
|
||||
|
||||
export function MessageImage(props: AttachmentProps) {
|
||||
const colors = useRosettaColors();
|
||||
@@ -25,9 +26,11 @@ export function MessageImage(props: AttachmentProps) {
|
||||
const preview = getPreview();
|
||||
const [blob, setBlob] = useState(props.attachment.blob);
|
||||
const [loadedImage, setLoadedImage] = useState(false);
|
||||
const [blurhashPreview, setBlurhashPreview] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
constructBlob();
|
||||
constructFromBlurhash();
|
||||
}, [downloadStatus]);
|
||||
|
||||
const constructBlob = async () => {
|
||||
@@ -45,6 +48,12 @@ export function MessageImage(props: AttachmentProps) {
|
||||
open(images, 0);
|
||||
}
|
||||
|
||||
const constructFromBlurhash = async () => {
|
||||
if (preview.length < 20) return;
|
||||
const blob = await blurhashToBase64Image(preview, 200, 220);
|
||||
setBlurhashPreview(blob);
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
if (downloadStatus == DownloadStatus.DOWNLOADED) {
|
||||
openImageViewer();
|
||||
@@ -83,7 +92,7 @@ export function MessageImage(props: AttachmentProps) {
|
||||
borderRadius: 8,
|
||||
objectFit: 'cover',
|
||||
border: '1px solid ' + colors.borderColor
|
||||
}} src={/*block render???*/blurhashToBase64Image(preview, 200, 220)}></img>
|
||||
}} src={blurhashPreview == "" ? undefined : blurhashPreview}></img>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
});
|
||||
}
|
||||
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