mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 08:03:42 +00:00
feat: setup the attachment to the passenger clients
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
isSupportAttachmentAllowed,
|
||||
isSupportAttachmentImage,
|
||||
SUPPORT_ATTACHMENT_MAX_BYTES,
|
||||
SUPPORT_ATTACHMENT_MAX_PER_MESSAGE,
|
||||
} from '@edr/types';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
/** A file staged in the composer, not yet sent. */
|
||||
export interface PendingAttachment {
|
||||
/** Local-only id; the server id doesn't exist until the message is sent. */
|
||||
id: string;
|
||||
file: File;
|
||||
/** Object URL, images only. Revoked when the entry goes away. */
|
||||
previewUrl?: string;
|
||||
}
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
/**
|
||||
* Staging area for files being attached to a message.
|
||||
*
|
||||
* Files are held client-side until send, then posted alongside the text in one
|
||||
* multipart request — there's no upload-then-reference step, so nothing to
|
||||
* garbage-collect if the agent changes their mind.
|
||||
*
|
||||
* Object URLs for image previews are revoked on removal and unmount; without
|
||||
* that, pasting screenshots into a long-lived chat page leaks the full bytes of
|
||||
* every image for the life of the tab.
|
||||
*/
|
||||
export function useAttachmentDraft(onReject?: (reason: string) => void) {
|
||||
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
|
||||
const rejectRef = useRef(onReject);
|
||||
rejectRef.current = onReject;
|
||||
|
||||
// Read from a ref in the unmount cleanup so it doesn't re-run (and revoke
|
||||
// still-live URLs) on every change to the list.
|
||||
const attachmentsRef = useRef(attachments);
|
||||
attachmentsRef.current = attachments;
|
||||
useEffect(
|
||||
() => () => {
|
||||
for (const a of attachmentsRef.current) {
|
||||
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const add = useCallback((files: File[]) => {
|
||||
if (files.length === 0) return;
|
||||
setAttachments((current) => {
|
||||
const accepted: PendingAttachment[] = [];
|
||||
for (const file of files) {
|
||||
if (current.length + accepted.length >= SUPPORT_ATTACHMENT_MAX_PER_MESSAGE) {
|
||||
rejectRef.current?.(`Up to ${SUPPORT_ATTACHMENT_MAX_PER_MESSAGE} files per message.`);
|
||||
break;
|
||||
}
|
||||
if (!isSupportAttachmentAllowed(file.type)) {
|
||||
rejectRef.current?.(`${file.name}: that file type isn't supported.`);
|
||||
continue;
|
||||
}
|
||||
if (file.size > SUPPORT_ATTACHMENT_MAX_BYTES) {
|
||||
rejectRef.current?.(
|
||||
`${file.name} is over the ${SUPPORT_ATTACHMENT_MAX_BYTES / (1024 * 1024)}MB limit.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
accepted.push({
|
||||
id: `pending-${nextId++}`,
|
||||
file,
|
||||
previewUrl: isSupportAttachmentImage(file.type) ? URL.createObjectURL(file) : undefined,
|
||||
});
|
||||
}
|
||||
return accepted.length ? [...current, ...accepted] : current;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const remove = useCallback((id: string) => {
|
||||
setAttachments((current) => {
|
||||
const target = current.find((a) => a.id === id);
|
||||
if (target?.previewUrl) URL.revokeObjectURL(target.previewUrl);
|
||||
return current.filter((a) => a.id !== id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setAttachments((current) => {
|
||||
for (const a of current) {
|
||||
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Pull files off a paste. Returns true if anything was taken, so the caller
|
||||
* can suppress the default paste — otherwise pasting a screenshot also drops
|
||||
* its filename (or nothing) into the textarea.
|
||||
*
|
||||
* Copying an image in most apps puts BOTH the bitmap and some text/html on
|
||||
* the clipboard, so check for files first and only then let the text through.
|
||||
*/
|
||||
const addFromPaste = useCallback(
|
||||
(clipboard: DataTransfer | null): boolean => {
|
||||
const files = Array.from(clipboard?.files ?? []);
|
||||
if (files.length === 0) return false;
|
||||
add(files);
|
||||
return true;
|
||||
},
|
||||
[add],
|
||||
);
|
||||
|
||||
return {
|
||||
attachments,
|
||||
files: attachments.map((a) => a.file),
|
||||
add,
|
||||
addFromPaste,
|
||||
remove,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user