import { useState , useEffect , useRef , useCallback , DragEvent , Ref , RefObject , forwardRef , useImperativeHandle , } from 'react' ; import { c } from 'ttag' ; import { Message } from '@proton/shared/lib/interfaces/mail/Message' ; import { getRecipients } from '@proton/shared/lib/mail/messages' ; import { classnames , useNotifications , useHandler , useSubscribeEventManager } from '@proton/components' ; import { noop } from '@proton/shared/lib/helpers/function' ; import { setBit , clearBit } from '@proton/shared/lib/helpers/bitset' ; import { EVENT_ACTIONS } from '@proton/shared/lib/constants' ; import { canonizeEmail } from '@proton/shared/lib/helpers/email' ; import { useDispatch } from 'react-redux' ; import { mergeMessages } from '../../helpers/message/messages' ; import { getContent , setContent } from '../../helpers/message/messageContent' ; import { useMessage , useGetMessage } from '../../hooks/message/useMessage' ; import { useInitializeMessage } from '../../hooks/message/useInitializeMessage' ; import { isNewDraft } from '../../helpers/message/messageDraft' ; import { useAttachments } from '../../hooks/composer/useAttachments' ; import { getDate } from '../../helpers/elements' ; import { Breakpoints } from '../../models/utils' ; import { EditorActionsRef } from './editor/SquireEditorWrapper' ; import { useHasScroll } from '../../hooks/useHasScroll' ; import { useReloadSendInfo , useMessageSendInfo } from '../../hooks/useSendInfo' ; import { DRAG_ADDRESS_KEY } from '../../constants' ; import { useComposerHotkeys } from '../../hooks/composer/useComposerHotkeys' ; import { ATTACHMENT_ACTION } from '../../helpers/attachment/attachmentUploader' ; import { useSendHandler } from '../../hooks/composer/useSendHandler' ; import { useCloseHandler } from '../../hooks/composer/useCloseHandler' ; import { updateKeyPackets } from '../../helpers/attachment/attachment' ; import { Event } from '../../models/event' ; import { replaceEmbeddedAttachments } from '../../helpers/message/messageEmbeddeds' ; import { useScheduleSend } from '../../hooks/composer/useScheduleSend' ; import { useHandleMessageAlreadySent } from '../../hooks/composer/useHandleMessageAlreadySent' ; import { useAutoSave } from '../../hooks/composer/useAutoSave' ; import { useLongLivingState } from '../../hooks/useLongLivingState' ; import ComposerInnerModals from './modals/ComposerInnerModals' ; import { ComposerInnerModalStates , useComposerInnerModals } from '../../hooks/composer/useComposerInnerModals' ; import { MessageState , MessageStateWithData , PartialMessageState } from '../../logic/messages/messagesTypes' ; import { removeInitialAttachments } from '../../logic/messages/draft/messagesDraftActions' ; import ComposerMeta from './ComposerMeta' ; import ComposerContent from './ComposerContent' ; import ComposerActions from './ComposerActions' ; export type MessageUpdate = PartialMessageState | ( ( message : MessageState ) => PartialMessageState ) ; export interface MessageChange { ( update , reloadSendInfo ? ) : void ; } export interface MessageChangeFlag { ( changes , reloadSendInfo ? ) : void ; } export interface ComposerAction { close : ( ) => void ; } interface Props { messageID : string ; composerFrameRef : RefObject < HTMLDivElement > ; breakpoints : Breakpoints ; toggleMinimized : ( ) => void ; toggleMaximized : ( ) => void ; onFocus : ( ) => void ; onClose : ( ) => void ; onSubject : ( subject : string ) => void ; isFocused : boolean ; } const Composer = ( { messageID , composerFrameRef , breakpoints , toggleMinimized , toggleMaximized , onFocus , onClose : inputOnClose , onSubject , isFocused , } , ref ) => { const dispatch = useDispatch ( ) ; const getMessage = useGetMessage ( ) ; const { createNotification } = useNotifications ( ) ; const bodyRef = useRef < HTMLDivElement > ( null ) ; const [ hasVerticalScroll ] = useHasScroll ( bodyRef ) ; const [ opening , setOpening ] = useState ( true ) ; const [ editorReady , setEditorReady ] = useState ( false ) ; const [ modelMessage , setModelMessage , getModelMessage ] = useLongLivingState < MessageState > ( { localID : messageID , } ) ; const messageSendInfo = useMessageSendInfo ( modelMessage ) ; const reloadSendInfo = useReloadSendInfo ( ) ; const { message : syncedMessage } = useMessage ( messageID ) ; const onClose = useHandler ( inputOnClose ) ; const onMessageAlreadySent = useHandleMessageAlreadySent ( { modelMessage , onClose , } ) ; const initialize = useInitializeMessage ( syncedMessage . localID ) ; const hasRecipients = getRecipients ( modelMessage . data ) . length > 0 ; const lock = opening ; const addressesBlurRef = useRef < ( ) => void > ( noop ) ; const addressesFocusRef = useRef < ( ) => void > ( noop ) ; const contentFocusRef = useRef < ( ) => void > ( noop ) ; const editorActionsRef = useRef ( ) ; const handleDragEnter = ( event ) => { if ( event . dataTransfer ?. types . includes ( DRAG_ADDRESS_KEY ) ) { onFocus ( ) ; } } ; const { autoSave , saveNow , deleteDraft , pendingSave , pendingAutoSave , pause : pauseAutoSave , restart : restartAutoSave , } = useAutoSave ( { onMessageAlreadySent } ) ; useEffect ( ( ) => { if ( ! pendingSave . isPending && ( syncedMessage . data ?. ID || ( ! syncedMessage . data ?. ID && ! isNewDraft ( syncedMessage . localID ) ) ) && syncedMessage . messageDocument ?. initialized === undefined && modelMessage . messageDocument ?. initialized === undefined ) { void initialize ( ) ; } } , [ pendingSave . isPending , syncedMessage . localID , syncedMessage . data ?. ID , syncedMessage . messageDocument ?. initialized , modelMessage . messageDocument ?. initialized , ] ) ; useEffect ( ( ) => { if ( modelMessage . data ?. ID !== syncedMessage . data ?. ID ) { const newModelMessage = { ... syncedMessage , ... modelMessage , data : { ... syncedMessage . data , ... modelMessage . data , Attachments : syncedMessage . data ?. Attachments , } as Message , messageImages : replaceEmbeddedAttachments ( modelMessage , syncedMessage . data ?. Attachments ) , } ; setModelMessage ( newModelMessage ) ; } else { const { changed , Attachments } = updateKeyPackets ( modelMessage , syncedMessage ) ; if ( changed ) { setModelMessage ( { ... modelMessage , data : { ... modelMessage . data , Attachments } as Message , messageImages : replaceEmbeddedAttachments ( modelMessage , Attachments ) , } ) ; } } } , [ pendingSave . isPending , syncedMessage . data ?. ID ] ) ; useEffect ( ( ) => { const firstInitialization = ! modelMessage . messageDocument ?. initialized && syncedMessage . messageDocument ?. initialized ; if ( firstInitialization ) { const isOpenFromUndo = syncedMessage . draftFlags ?. openDraftFromUndo === true ; const password = isOpenFromUndo ? { } : { Password : undefined , PasswordHint : undefined } ; const newModelMessage = { ... syncedMessage , ... modelMessage , data : { ... syncedMessage . data , ... password , ... modelMessage . data , Attachments : syncedMessage . data ?. Attachments , } as Message , messageDocument : { ... syncedMessage . messageDocument , initialized : true , } , messageImages : syncedMessage . messageImages , } ; setModelMessage ( newModelMessage ) ; void reloadSendInfo ( messageSendInfo , newModelMessage ) ; } } , [ pendingSave . isPending , syncedMessage . messageDocument ?. document , syncedMessage . messageDocument ?. plainText , syncedMessage . messageDocument ?. initialized , ] ) ; const timeoutRef = useRef ( 0 ) ; useEffect ( ( ) => { if ( ! opening && isFocused ) { timeoutRef . current = window . setTimeout ( ( ) => { if ( getRecipients ( syncedMessage . data ) . length === 0 ) { addressesFocusRef . current ( ) ; } else { contentFocusRef . current ( ) ; } } ) ; } return ( ) => { if ( timeoutRef . current ) { clearTimeout ( timeoutRef . current ) ; } } ; } , [ opening , isFocused ] ) ; useEffect ( ( ) => { onSubject ( modelMessage . data ?. Subject || c ( 'Title' ) . t `New message` ) ; } , [ modelMessage . data ?. Subject ] ) ; useSubscribeEventManager ( ( { Contacts = [ ] } ) => { if ( ! Contacts . length ) { return ; } let shouldReloadSendInfo = false ; const updatedAddresses = Contacts . map ( ( { Action , Contact } ) => { if ( Action === EVENT_ACTIONS . DELETE ) { shouldReloadSendInfo = true ; } return Contact ?. ContactEmails . map ( ( { Email } ) => canonizeEmail ( Email ) ) || [ ] ; } ) . flat ( ) ; const recipientsAddresses = getRecipients ( modelMessage . data ) . map ( ( { Address } ) => canonizeEmail ( Address ) ) ; const matches = updatedAddresses . find ( ( address ) => recipientsAddresses . includes ( address ) ) ; shouldReloadSendInfo = shouldReloadSendInfo || ! ! matches ; if ( shouldReloadSendInfo ) { void reloadSendInfo ( messageSendInfo , modelMessage ) ; } } ) ; const handleChange = useHandler ( ( update , shouldReloadSendInfo ) => { setModelMessage ( ( modelMessage ) => { const messageChanges = update instanceof Function ? update ( modelMessage ) : update ; const newModelMessage = mergeMessages ( modelMessage , messageChanges ) ; if ( shouldReloadSendInfo ) { void reloadSendInfo ( messageSendInfo , newModelMessage ) ; } void autoSave ( newModelMessage ) ; return newModelMessage ; } ) ; } ) ; const handleChangeContent = useHandler ( ( content , refreshEditor = false , silent = false ) => { setModelMessage ( ( modelMessage ) => { setContent ( modelMessage , content ) ; const newModelMessage = { ... modelMessage } ; if ( ! silent ) { void autoSave ( newModelMessage ) ; } if ( refreshEditor ) { editorActionsRef . current ?. setContent ( newModelMessage ) ; } return newModelMessage ; } ) ; } ) ; const handleChangeFlag = useHandler ( ( changes , shouldReloadSendInfo = false ) => { handleChange ( ( message ) => { let Flags = message . data ?. Flags || 0 ; changes . forEach ( ( isAdd , flag ) => { const action = isAdd ? setBit : clearBit ; Flags = action ( Flags , flag ) ; } ) ; return { data : { Flags } } ; } , shouldReloadSendInfo ) ; } ) ; /**
     * In some rare situations, Squire can miss an input event.
     * A missed event can lead to not sending the expected content which is serious.
     * This function perform an ultimate content check before sending especially.
     */ const ensureMessageContent = ( ) => { if ( ! editorActionsRef . current ) { return true ; } const actualContent = editorActionsRef . current . getContent ( ) ; const modelContent = getContent ( modelMessage ) ; if ( actualContent . trim ( ) !== modelContent . trim ( ) ) { handleChangeContent ( actualContent ) ; } } ; /**
     * Ensure the draft is saved before continue
     */ const handleSaveNow = async ( ) => { if ( ! modelMessage . data ?. ID ) { return saveNow ( modelMessage ) ; } } ; const { pendingFiles , pendingUploads , promiseUpload , uploadInProgress , handleAddAttachmentsStart , handleAddAttachmentsUpload , handleCancelAddAttachment , handleRemoveAttachment , handleRemoveUpload , } = useAttachments ( { message : modelMessage , onChange : handleChange , onSaveNow : handleSaveNow , editorActionsRef , onMessageAlreadySent , } ) ; useEffect ( ( ) => { const attachmentToUpload = ! ! syncedMessage . draftFlags ?. initialAttachments ?. length ; if ( attachmentToUpload ) { const uploadInitialAttachments = async ( ) => { const files = syncedMessage . draftFlags ?. initialAttachments ; dispatch ( removeInitialAttachments ( messageID ) ) ; await saveNow ( syncedMessage ) ; await handleAddAttachmentsUpload ( ATTACHMENT_ACTION . ATTACHMENT , files ) ; } ; void uploadInitialAttachments ( ) ; } if ( editorReady && syncedMessage . messageDocument ?. initialized && ! attachmentToUpload ) { setOpening ( false ) ; } } , [ editorReady , syncedMessage . data , syncedMessage . messageDocument ?. initialized ] ) ; useEffect ( ( ) => { if ( uploadInProgress ) { pauseAutoSave ( ) ; } else { restartAutoSave ( ) ; } } , [ uploadInProgress ] ) ; const handleDiscard = async ( ) => { const message = getMessage ( messageID ) ; if ( message ?. data ?. ID ) { await deleteDraft ( message ) ; } createNotification ( { text : c ( 'Info' ) . t `Draft discarded` } ) ; } ; const handleDelete = async ( ) => { autoSave . abort ?. ( ) ; try { await handleDiscard ( ) ; } finally { onClose ( ) ; } } ; const { saving , handleManualSave , handleClose } = useCloseHandler ( { syncedMessage , modelMessage , lock , ensureMessageContent , autoSave , saveNow , onClose , onDiscard : handleDiscard , pendingAutoSave , promiseUpload , uploadInProgress , onMessageAlreadySent , } ) ; const { innerModal , setInnerModal , attachmentsFoundKeyword , handlePassword , handleExpiration , handleCloseInnerModal , handleDeleteDraft , handleNoRecipients , handleNoSubjects , handleNoAttachments , handleCloseInsertImageModal , handleSendAnyway , handleCancelSend , } = useComposerInnerModals ( { pendingFiles , handleCancelAddAttachment , } ) ; const handleSend = useSendHandler ( { getModelMessage , ensureMessageContent , mapSendInfo : messageSendInfo . mapSendInfo , promiseUpload , pendingSave , pendingAutoSave , autoSave , saveNow , onClose , onMessageAlreadySent , handleNoRecipients , handleNoSubjects , handleNoAttachments , } ) ; const { loadingScheduleCount , handleScheduleSendModal , handleScheduleSend } = useScheduleSend ( { modelMessage : modelMessage as MessageStateWithData , setInnerModal , ComposerInnerModal : ComposerInnerModalStates , setModelMessage , handleSend , handleNoRecipients , handleNoSubjects , handleNoAttachments , } ) ; useImperativeHandle ( ref , ( ) => ( { close : handleClose , } ) ) ; const handleEditorReady = useCallback ( ( ) => setEditorReady ( true ) , [ ] ) ; const handleContentFocus = useCallback ( ( ) => { addressesBlurRef . current ( ) ; onFocus ( ) ; } , [ ] ) ; const { squireKeydownHandler , attachmentTriggerRef } = useComposerHotkeys ( { composerRef : composerFrameRef , handleClose , handleDelete , handleExpiration , handleManualSave , handlePassword , handleSend , toggleMinimized , toggleMaximized , lock : lock || ! hasRecipients , saving , } ) ; return ( < div className = "composer-container flex flex-column flex-item-fluid relative w100" onDragEnter = { handleDragEnter } > < ComposerInnerModals innerModal = { innerModal } message = { modelMessage } attachmentsFoundKeyword = { attachmentsFoundKeyword } handleChange = { handleChange } pendingFiles = { pendingFiles } handleCloseInnerModal = { handleCloseInnerModal } handleScheduleSend = { handleScheduleSend } handleCloseInsertImageModal = { handleCloseInsertImageModal } handleAddAttachmentsUpload = { handleAddAttachmentsUpload } handleDelete = { handleDelete } handleSendAnyway = { handleSendAnyway } handleCancelSend = { handleCancelSend } / > < div className = { classnames ( [ 'composer-blur-container flex flex-column flex-item-fluid max-w100' , innerModal === ComposerInnerModalStates . None ? 'flex' : 'hidden' , ] ) } > < div ref = { bodyRef } className = "composer-body-container flex flex-column flex-nowrap flex-item-fluid max-w100 mt0-5" > < ComposerMeta message = { modelMessage } messageSendInfo = { messageSendInfo } disabled = { lock } onChange = { handleChange } onChangeContent = { handleChangeContent } addressesBlurRef = { addressesBlurRef } addressesFocusRef = { addressesFocusRef } / > < ComposerContent message = { modelMessage } disabled = { lock } breakpoints = { breakpoints } onEditorReady = { handleEditorReady } onChange = { handleChange } onChangeContent = { handleChangeContent } onFocus = { handleContentFocus } onAddAttachments = { handleAddAttachmentsStart } onRemoveAttachment = { handleRemoveAttachment } onRemoveUpload = { handleRemoveUpload } pendingUploads = { pendingUploads } contentFocusRef = { contentFocusRef } editorActionsRef = { editorActionsRef } squireKeydownHandler = { squireKeydownHandler } / > < /div> < ComposerActions className = { hasVerticalScroll ? 'composer-actions--has-scroll' : undefined } message = { modelMessage } date = { getDate ( syncedMessage . data , '' ) } lock = { lock } opening = { opening } syncInProgress = { pendingSave . isPending } onAddAttachments = { handleAddAttachmentsStart } onExpiration = { handleExpiration } onPassword = { handlePassword } onScheduleSendModal = { handleScheduleSendModal } onSend = { handleSend } onDelete = { handleDeleteDraft } addressesBlurRef = { addressesBlurRef } attachmentTriggerRef = { attachmentTriggerRef } loadingScheduleCount = { loadingScheduleCount } onChangeFlag = { handleChangeFlag } / > < /div> < /div>); } ; export default forwardRef ( Composer ) ;