// End of third-party imports import { compact } from 'lodash' import { FileCode, Plus, X } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type RefObject, } from 'react' import { toast } from 'sonner' import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { createSupportStorageClient } from './support-storage-client' import { InlineLink } from '@/components/ui/InlineLink' import { useGenerateAttachmentURLsMutation } from '@/data/support/generate-attachment-urls-mutation' import { uuidv4 } from '@/lib/helpers' import { useProfile } from '@/lib/profile' const MAX_ATTACHMENTS = 5 const uploadAttachments = async ({ userId, files }: { userId: string; files: File[] }) => { const supportBrivenClient = createSupportStorageClient() const filesToUpload = Array.from(files) const uploadedFiles = await Promise.all( filesToUpload.map(async (file) => { const suffix = file.name.endsWith('.har') ? 'har' : file.type.split('/')[1] const prefix = `${userId}/${uuidv4()}.${suffix}` const options = { cacheControl: '3600' } const { data, error } = await supportBrivenClient.storage .from('support-attachments') .upload(prefix, file, options) if (error) console.error('Failed to upload:', file.name, error) return data }) ) const keys = compact(uploadedFiles).map((file) => file.path) return keys } export function useAttachmentUpload() { const { profile } = useProfile() const uploadButtonRef = useRef(null) const [uploadedFiles, setUploadedFiles] = useState([]) const [uploadedDataUrls, setUploadedDataUrls] = useState([]) const { mutateAsync: generateAttachmentURLs } = useGenerateAttachmentURLsMutation() const isFull = uploadedFiles.length >= MAX_ATTACHMENTS const addFile = useCallback(() => { uploadButtonRef.current?.click() }, []) const handleFileUpload = useCallback( async (event: ChangeEvent) => { event.persist() const items = event.target.files || (event as any).dataTransfer.items const itemsCopied = Array.prototype.map.call(items, (item: any) => item) as File[] const itemsToBeUploaded = itemsCopied.slice(0, MAX_ATTACHMENTS - uploadedFiles.length) setUploadedFiles(uploadedFiles.concat(itemsToBeUploaded)) if (items.length + uploadedFiles.length > MAX_ATTACHMENTS) { toast(`Only up to ${MAX_ATTACHMENTS} attachments are allowed`) } event.target.value = '' }, [uploadedFiles] ) const removeFileUpload = useCallback( (idx: number) => { const updatedFiles = uploadedFiles.slice() updatedFiles.splice(idx, 1) setUploadedFiles(updatedFiles) const updatedDataUrls = uploadedDataUrls.slice() uploadedDataUrls.splice(idx, 1) setUploadedDataUrls(updatedDataUrls) }, [uploadedFiles, uploadedDataUrls] ) useEffect(() => { if (!uploadedFiles) return const objectUrls = uploadedFiles.map((file) => { if (file.name.endsWith('.har')) { return file.name } else { return URL.createObjectURL(file) } }) setUploadedDataUrls(objectUrls) return () => { objectUrls.forEach((url: any) => void URL.revokeObjectURL(url)) } }, [uploadedFiles]) const createAttachments = useCallback(async () => { if (!profile?.id) { console.error('[Support Form > uploadAttachments] Unable to upload files, missing user ID') toast.error('Unable to upload attachments') return [] } if (uploadedFiles.length === 0) return try { const filenames = await uploadAttachments({ userId: profile.gotrue_id, files: uploadedFiles }) const urls = await generateAttachmentURLs({ bucket: 'support-attachments', filenames }) return urls } catch { // Ignore attachments upload errors, images are additional context and support can ask for more if needed return } // eslint-disable-next-line react-hooks/exhaustive-deps }, [profile, uploadedFiles]) return useMemo( () => ({ uploadButtonRef, isFull, addFile, handleFileUpload, removeFileUpload, createAttachments, uploadedDataUrls, }), [isFull, addFile, handleFileUpload, removeFileUpload, createAttachments, uploadedDataUrls] ) } interface AttachmentUploadDisplayProps { uploadButtonRef: RefObject isFull: boolean uploadedDataUrls: string[] addFile: () => void handleFileUpload: (event: ChangeEvent) => Promise removeFileUpload: (idx: number) => void } export function AttachmentUploadDisplay({ uploadButtonRef, isFull, uploadedDataUrls, addFile, handleFileUpload, removeFileUpload, }: AttachmentUploadDisplayProps) { const { profile } = useProfile() if (!profile) { return (

Attachments

Uploads are only supported when logged in. Please reply to the acknowledgement email you will receive with any screenshots you'd like to upload.

) } return (

Attachments

Optionally upload up to {MAX_ATTACHMENTS} relevant images or{' '} HAR files

{uploadedDataUrls.map((url, idx) => { if (url.endsWith('.har')) { return (
e.preventDefault()}>

HAR

{url}
) } else { return (
) } })} {!isFull && ( )}
) }