Исправлена блокировка потока при вставке изображений, оптимизирован код и ответственность

This commit is contained in:
RoyceDa
2026-02-19 21:43:27 +02:00
parent a38a331cd1
commit 53535d68e0
7 changed files with 147 additions and 54 deletions

View File

@@ -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
}]);

View File

@@ -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>

View File

@@ -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>
</>
)}

View File

@@ -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;

View File

@@ -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;
});
}

View 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...

View 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) });
}
};