import { Attachment } from '@proton/shared/lib/interfaces/mail/Message' ; import { getAttachments , isPlainText } from '@proton/shared/lib/mail/messages' ; import { useState } from 'react' ; import { useApi , useNotifications , useAuthentication , useHandler } from '@proton/components' ; import { c } from 'ttag' ; import { removeAttachment } from '@proton/shared/lib/api/attachments' ; import { readFileAsBuffer } from '@proton/shared/lib/helpers/file' ; import { useDispatch } from 'react-redux' ; import { Upload } from '../../helpers/upload' ; import { UploadResult , ATTACHMENT_ACTION , isSizeExceeded , upload } from '../../helpers/attachment/attachmentUploader' ; import { EditorActionsRef } from '../../components/composer/editor/SquireEditorWrapper' ; import { MessageChange } from '../../components/composer/Composer' ; import { useGetMessageKeys } from '../message/useGetMessageKeys' ; import { useLongLivingState } from '../useLongLivingState' ; import { usePromise } from '../usePromise' ; import { getEmbeddedImages , updateImages } from '../../helpers/message/messageImages' ; import { createEmbeddedImageFromUpload , isEmbeddable , matchSameCidOrLoc , readContentIDandLocation , } from '../../helpers/message/messageEmbeddeds' ; import { MESSAGE_ALREADY_SENT_INTERNAL_ERROR } from '../../constants' ; import { addAttachment } from '../../logic/attachments/attachmentsActions' ; import { MessageState , MessageStateWithData } from '../../logic/messages/messagesTypes' ; import { useGetMessage } from '../message/useMessage' ; export interface PendingUpload { file : File ; upload : Upload < UploadResult > ; } interface UseAttachmentsParameters { message : MessageState ; onChange : MessageChange ; onSaveNow : ( ) => Promise < void > ; editorActionsRef : EditorActionsRef ; onMessageAlreadySent : ( ) => void ; } export const useAttachments = ( { message , onChange , onSaveNow , editorActionsRef , onMessageAlreadySent , } ) => { const api = useApi ( ) ; const { createNotification } = useNotifications ( ) ; const auth = useAuthentication ( ) ; const getMessage = useGetMessage ( ) ; const getMessageKeys = useGetMessageKeys ( ) ; const dispatch = useDispatch ( ) ; const [ pendingFiles , setPendingFiles ] = useState < File [ ] > ( ) ; const [ pendingUploads , setPendingUploads , getPendingUpload ] = useLongLivingState < PendingUpload [ ] > ( [ ] ) ; const { promise : promiseUpload , resolver : resolveUpload , rejecter : rejectUpload , renew : newUpload , isPending : uploadInProgress , } = usePromise < void > ( ) ; const { localID } = message ; const updatePendingUpload = useHandler ( ( pendingUploads , error ? ) => { const previousValue = getPendingUpload ( ) ; setPendingUploads ( pendingUploads ) ; if ( error ) { rejectUpload ( error ) ; } else if ( ! pendingUploads . length && previousValue . length ) { resolveUpload ( ) ; } } ) ; const addPendingUploads = ( pendingUploads ) => { updatePendingUpload ( [ ... getPendingUpload ( ) , ... pendingUploads ] ) ; } ; const removePendingUpload = ( pendingUpload , error ? ) => { updatePendingUpload ( getPendingUpload ( ) . filter ( ( aPendingUpload ) => aPendingUpload !== pendingUpload ) , error ) ; } ; const ensureMessageIsCreated = async ( ) => { await onSaveNow ( ) ; return getMessage ( localID ) as MessageStateWithData ; } ; /**
     * Wait for upload to finish, modify the message, add to embedded images if needed
     */ const handleAddAttachmentEnd = useHandler ( async ( action , pendingUpload ) => { try { const upload = await pendingUpload . upload . resultPromise ; const data = new Uint8Array ( await readFileAsBuffer ( pendingUpload . file ) ) ; const filename = pendingUpload . file . name ; dispatch ( addAttachment ( { ID : upload . attachment . ID || '' , attachment : { data , verified : 1 , filename , signatures : [ ] } , } ) ) ; onChange ( ( message ) => { const Attachments = [ ... getAttachments ( message . data ) , upload . attachment ] ; const embeddedImages = getEmbeddedImages ( message ) ; if ( action === ATTACHMENT_ACTION . INLINE ) { embeddedImages . push ( createEmbeddedImageFromUpload ( upload ) ) ; } const messageImages = updateImages ( message . messageImages , undefined , undefined , embeddedImages ) ; return { data : { Attachments } , messageImages } ; } ) ; if ( action === ATTACHMENT_ACTION . INLINE ) { editorActionsRef . current ?. insertEmbedded ( upload . attachment , upload . packets . Preview ) ; } removePendingUpload ( pendingUpload ) ; } catch ( error ) { if ( error ?. message === MESSAGE_ALREADY_SENT_INTERNAL_ERROR ) { onMessageAlreadySent ( ) ; } removePendingUpload ( pendingUpload , error ) ; } } ) ; const checkSize = ( files ) => { const pendingUploadFiles = pendingUploads . map ( ( upload ) => upload . file ) ; const sizeExcedeed = isSizeExceeded ( message , [ ... files , ... pendingUploadFiles ] ) ; if ( sizeExcedeed ) { createNotification ( { type : 'error' , text : c ( 'Error' ) . t `Attachments are limited to 25 MB` , } ) ; } return sizeExcedeed ; } ; /**
     * Start uploading a file, the choice between attachment or inline is done.
     */ const handleAddAttachmentsUpload = useHandler ( async ( action , files = pendingFiles || [ ] ) => { setPendingFiles ( undefined ) ; if ( getPendingUpload ( ) . length === 0 ) { newUpload ( ) ; } const messageFromState = await ensureMessageIsCreated ( ) ; const messageKeys = await getMessageKeys ( messageFromState . data ) ; if ( messageFromState . draftFlags ?. isSentDraft ) { onMessageAlreadySent ( ) ; return ; } const uploads = upload ( files , messageFromState , messageKeys , action , auth . UID ) ; const pendingUploads = files . map ( ( file , i ) => ( { file , upload : uploads [ i ] } ) ) ; addPendingUploads ( pendingUploads ) ; pendingUploads . forEach ( ( pendingUpload ) => handleAddAttachmentEnd ( action , pendingUpload ) ) ; } ) ; /**
     * Trigger an directly an embedded upload.
     */ const handleAddEmbeddedImages = async ( files ) => { if ( checkSize ( files ) ) { return ; } void handleAddAttachmentsUpload ( ATTACHMENT_ACTION . INLINE , files ) ; } ; /**
     * Entry point for upload, will check and ask for attachment action if possible
     */ const handleAddAttachmentsStart = useHandler ( async ( files ) => { const embeddable = files . every ( ( file ) => isEmbeddable ( file . type ) ) ; const plainText = isPlainText ( message . data ) ; if ( checkSize ( files ) ) { return ; } if ( ! plainText && embeddable ) { setPendingFiles ( files ) ; } else { void handleAddAttachmentsUpload ( ATTACHMENT_ACTION . ATTACHMENT , files ) ; } } ) ; const handleCancelAddAttachment = ( ) => setPendingFiles ( undefined ) ; /**
     * Remove an existing attachment, deal with potential embedded image
     */ const handleRemoveAttachment = useHandler ( async ( attachment ) => { if ( attachment . ID && message . data ?. ID ) { await api ( removeAttachment ( attachment . ID , message . data . ID ) ) ; } onChange ( ( message ) => { const Attachments = message . data ?. Attachments ?. filter ( ( a ) => a . ID !== attachment . ID ) || [ ] ; const { cid , cloc } = readContentIDandLocation ( attachment ) ; const embeddedImages = getEmbeddedImages ( message ) ; const embeddedImage = embeddedImages . find ( ( image ) => matchSameCidOrLoc ( image , cid , cloc ) ) ; const newEmbeddedImages = embeddedImages . filter ( ( image ) => ! matchSameCidOrLoc ( image , cid , cloc ) ) ; if ( embeddedImage ) { setTimeout ( ( ) => { editorActionsRef . current ?. removeEmbedded ( embeddedImage . attachment ) ; } ) ; } const messageImages = updateImages ( message . messageImages , undefined , undefined , newEmbeddedImages ) ; return { data : { Attachments } , messageImages } ; } ) ; } ) ; /**
     * Cancel pending upload
     */ const handleRemoveUpload = async ( pendingUpload ) => { pendingUpload . upload . abort ( ) ; void removePendingUpload ( pendingUpload ) ; } ; return { pendingFiles , pendingUploads , promiseUpload , uploadInProgress , handleAddAttachmentsStart , handleAddEmbeddedImages , handleAddAttachmentsUpload , handleCancelAddAttachment , handleRemoveAttachment , handleRemoveUpload , } ; } ;