import { AttachFile, Close, DeleteOutline, DocumentScanner, Image, InsertDriveFile, PhotoCamera, } from '@mui/icons-material' import { Box, Button, CircularProgress, IconButton, Sheet, Typography, } from '@mui/joy' import { ClickAwayListener, Popper } from '@mui/material' import { useEffect, useRef, useState } from 'react' import { useDocumentScanner } from '../../hooks/useDocumentScanner' import { useFileUpload } from '../../hooks/useFileUpload' import { useNotification } from '../../service/NotificationProvider' import { DeleteDraftAttachment } from '../../utils/Fetcher' import { imageSourceToFile } from '../../utils/FileConvert' const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'] const isImageAttachment = attachment => { const ext = (attachment?.name || '').split('.').pop()?.toLowerCase() return IMAGE_EXTENSIONS.includes(ext) } const AttachmentPickerField = ({ attachments = [], draftId, emptyDisplay = 'icon-text', entityId, entityType = 'chore_attachment', onChange, onClear, }) => { const [isOpen, setIsOpen] = useState(false) const [isUploading, setIsUploading] = useState(false) const buttonRef = useRef(null) const { uploadFile } = useFileUpload({ entityType, entityId, draftId }) const { isNativeScanner, scanDocument } = useDocumentScanner() const { showError } = useNotification() // Without a native scanner, `capture` asks a phone for its camera directly. // Desktop browsers ignore it and fall back to the file picker, which would // duplicate "Image", so the button only appears on touch devices. const canTakePhoto = isNativeScanner || navigator.maxTouchPoints > 0 useEffect(() => { if (!isOpen) return const handleEscape = e => { if (e.key === 'Escape') setIsOpen(false) } document.addEventListener('keydown', handleEscape) return () => document.removeEventListener('keydown', handleEscape) }, [isOpen]) const upload = async file => { setIsUploading(true) try { const uploaded = await uploadFile(file) if (uploaded) { onChange([ ...attachments, { url: uploaded.url, path: uploaded.path, name: uploaded.fileName }, ]) } } finally { setIsUploading(false) } } const handlePickFile = ({ accept, capture } = {}) => { const input = document.createElement('input') input.setAttribute('type', 'file') if (accept) input.setAttribute('accept', accept) if (capture) input.setAttribute('capture', capture) input.onchange = () => { const file = input.files?.[0] if (file) upload(file) } input.click() } // Native builds get the OS document scanner (edge detection + perspective // correction); everywhere else "take photo" is the camera roll shortcut. const handleScan = async () => { if (!isNativeScanner) { handlePickFile({ accept: 'image/*', capture: 'environment' }) return } const { cancelled, error, image } = await scanDocument() if (cancelled) return if (error || !image) { showError({ title: 'Scan Failed', message: error || 'Could not scan the document.', }) return } setIsUploading(true) const file = await imageSourceToFile(image, `scan-${Date.now()}.jpg`) setIsUploading(false) if (!file) { showError({ title: 'Scan Failed', message: 'Could not read the scanned image.', }) return } await upload(file) } const handleRemove = async index => { const attachment = attachments[index] // Draft uploads exist server-side too — delete there so they are not // promoted onto the chore when it is created. if (attachment?.path) { try { await DeleteDraftAttachment(attachment.path) } catch { // file may already be gone; still drop it from the list } } const updated = attachments.filter((_, i) => i !== index) onChange(updated) if (updated.length === 0) setIsOpen(false) } const handleClear = e => { e.stopPropagation() onClear?.() setIsOpen(false) } const isEmpty = attachments.length === 0 const shouldShowLabel = !isEmpty || emptyDisplay === 'icon-text' return ( <> {!isEmpty && onClear && ( )} {isOpen && ( setIsOpen(false)}> {attachments.length > 0 && ( {attachments.map((attachment, index) => ( {isImageAttachment(attachment) && ( { e.target.style.display = 'none' e.target.nextSibling.style.display = 'flex' }} /> )} {isImageAttachment(attachment) ? ( ) : ( )} {attachment.name} handleRemove(index)} sx={{ flexShrink: 0 }} > ))} )} {isUploading ? ( ) : ( {canTakePhoto && ( )} )} )} ) } export default AttachmentPickerField