Files
edr-platform/apps/edr-freight-web/backoffice/src/features/support/MessageAttachments.tsx
2026-07-18 08:52:06 +00:00

123 lines
3.4 KiB
TypeScript

import {
isSupportAttachmentImage,
type SupportAttachmentDto,
} from "@edr/types";
import { Box, Group, Image, Loader, Paper, Stack, Text } from "@mantine/core";
import { FileText, ImageOff } from "lucide-react";
import { useAttachmentObjectUrl } from "./useAttachmentObjectUrl";
/** 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`;
}
/** Cap the bubble: a tall screenshot would push the conversation off-screen. */
const THUMB = { maxHeight: 220, maxWidth: 260 } as const;
/**
* 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: SupportAttachmentDto;
onView: (a: SupportAttachmentDto, src: string) => void;
}) {
const { src, failed } = useAttachmentObjectUrl(a.url);
if (failed) {
return (
<Group gap={6} c="dimmed">
<ImageOff size={14} />
<Text size="xs">Couldn't load {a.name}</Text>
</Group>
);
}
if (!src) {
return (
<Box
style={{
width: THUMB.maxWidth,
height: 140,
display: "grid",
placeItems: "center",
background: "var(--mantine-color-gray-1)",
borderRadius: 8,
}}
>
<Loader size="xs" color="edr-green" />
</Box>
);
}
return (
<Box
onClick={() => onView(a, src)}
style={{ cursor: "zoom-in", borderRadius: 8, overflow: "hidden" }}
>
<Image src={src} alt={a.name} radius="md" fit="cover" style={THUMB} />
</Box>
);
}
/**
* 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: SupportAttachmentDto[];
mine: boolean;
onView: (a: SupportAttachmentDto, src: string) => void;
onOpenFile: (a: SupportAttachmentDto) => void;
}) {
if (attachments.length === 0) return null;
return (
<Stack gap={6} mt={6}>
{attachments.map((a) =>
isSupportAttachmentImage(a.mimeType) ? (
<ImageAttachment key={a.id} a={a} onView={onView} />
) : (
<Paper
key={a.id}
onClick={() => onOpenFile(a)}
px="sm"
py={6}
radius="md"
style={{
cursor: "pointer",
background: mine ? "rgba(255,255,255,0.16)" : "white",
border: mine ? "none" : "1px solid var(--mantine-color-gray-3)",
}}
>
<Group gap={8} wrap="nowrap">
<FileText size={16} style={{ flexShrink: 0 }} />
<Box style={{ minWidth: 0 }}>
<Text size="xs" fw={600} truncate>
{a.name}
</Text>
<Text size="10px" opacity={0.75}>
{formatBytes(a.size)}
</Text>
</Box>
</Group>
</Paper>
),
)}
</Stack>
);
}