mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
feat: setup the attachment to the passenger clients
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { isSupportAttachmentImage, type Passenger } from '@edr/types';
|
||||
import { FileText, ImageOff } from 'lucide-react';
|
||||
|
||||
import { useAttachmentObjectUrl } from './useAttachmentObjectUrl';
|
||||
|
||||
type AttachmentDto = Passenger.PassengerSupportAttachmentDto;
|
||||
|
||||
/** Human-readable size — kept coarse; nobody needs bytes in a chat bubble. */
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
/**
|
||||
* One image attachment. Its own component because the bytes are fetched through
|
||||
* the authenticated client (see {@link useAttachmentObjectUrl}) and a hook can't
|
||||
* be called from inside a `.map()`.
|
||||
*/
|
||||
function ImageAttachment({
|
||||
a,
|
||||
onView,
|
||||
}: {
|
||||
a: AttachmentDto;
|
||||
onView: (a: AttachmentDto, src: string) => void;
|
||||
}) {
|
||||
const { src, failed } = useAttachmentObjectUrl(a.url);
|
||||
|
||||
if (failed) {
|
||||
return (
|
||||
<span className="flex items-center gap-1.5 text-xs opacity-75">
|
||||
<ImageOff size={14} className="shrink-0" />
|
||||
Couldn't load {a.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!src) {
|
||||
return (
|
||||
<div className="h-[120px] w-[200px] animate-pulse rounded-lg bg-black/10 dark:bg-white/10" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onView(a, src)}
|
||||
className="cursor-zoom-in overflow-hidden rounded-lg"
|
||||
aria-label={`View ${a.name}`}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={a.name}
|
||||
// Cap the bubble: a tall screenshot would otherwise push the
|
||||
// whole conversation off-screen.
|
||||
className="max-h-[220px] max-w-[260px] rounded-lg object-cover"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachments inside a message bubble: images as thumbnails, everything else as
|
||||
* a labelled file row. Non-images are not fetched until opened — pulling every
|
||||
* document in a thread just to draw a filename would be wasteful.
|
||||
*/
|
||||
export function MessageAttachments({
|
||||
attachments,
|
||||
mine,
|
||||
onView,
|
||||
onOpenFile,
|
||||
}: {
|
||||
attachments: AttachmentDto[];
|
||||
mine: boolean;
|
||||
onView: (a: AttachmentDto, src: string) => void;
|
||||
onOpenFile: (a: AttachmentDto) => void;
|
||||
}) {
|
||||
if (attachments.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-1.5 flex flex-col gap-1.5">
|
||||
{attachments.map((a) =>
|
||||
isSupportAttachmentImage(a.mimeType) ? (
|
||||
<ImageAttachment key={a.id} a={a} onView={onView} />
|
||||
) : (
|
||||
<button
|
||||
key={a.id}
|
||||
onClick={() => onOpenFile(a)}
|
||||
className={`flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition hover:opacity-90 ${
|
||||
mine ? 'bg-white/20' : 'border border-border bg-background'
|
||||
}`}
|
||||
>
|
||||
<FileText size={16} className="shrink-0" />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-xs font-semibold">{a.name}</span>
|
||||
<span className="block text-[10px] opacity-75">{formatBytes(a.size)}</span>
|
||||
</span>
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user