import { Attachment } from '@proton/shared/lib/interfaces/mail/Message' ; import { isPlainText as testIsPlainText } from '@proton/shared/lib/mail/messages' ; import { MutableRefObject , useEffect , useState , useRef , useMemo , useCallback , memo } from 'react' ; import { c } from 'ttag' ; import { SquireEditor , useHandler , useMailSettings , useAddresses , FontData } from '@proton/components' ; import { SquireEditorMetadata } from '@proton/components/components/editor/interface' ; import { SquireEditorRef } from '@proton/components/components/editor/SquireEditor' ; import { RIGHT_TO_LEFT , MIME_TYPES } from '@proton/shared/lib/constants' ; import { diff } from '@proton/shared/lib/helpers/array' ; import { noop } from '@proton/shared/lib/helpers/function' ; import useIsMounted from '@proton/components/hooks/useIsMounted' ; import { defaultFontStyle , DEFAULT_FONT_FACE , DEFAULT_FONT_SIZE , } from '@proton/components/components/editor/squireConfig' ; import { Breakpoints } from '../../../models/utils' ; import { MessageChange } from '../Composer' ; import { getContent , exportPlainText , plainTextToHTML , setDocumentContent , } from '../../../helpers/message/messageContent' ; import { locateBlockquote } from '../../../helpers/message/messageBlockquote' ; import { getEmbeddedImages } from '../../../helpers/message/messageImages' ; import { createBlob , findCIDsInContent , readContentIDandLocation , removeEmbeddedHTML , } from '../../../helpers/message/messageEmbeddeds' ; import { MessageState } from '../../../logic/messages/messagesTypes' ; interface ExternalEditorActions { getContent : ( ) => string ; setContent : ( message : MessageState ) => void ; insertEmbedded : ( attachment : Attachment , data : string | Uint8Array ) => void ; removeEmbedded : ( attachment : Attachment ) => void ; } export type EditorActionsRef = MutableRefObject < ExternalEditorActions | undefined > ; interface Props { message : MessageState ; disabled : boolean ; breakpoints : Breakpoints ; onReady : ( ) => void ; onChange : MessageChange ; onChangeContent : ( content : string , refreshEditor ? : boolean , silent ? : boolean ) => void ; onFocus : ( ) => void ; onAddAttachments : ( files : File [ ] ) => void ; onRemoveAttachment : ( attachment : Attachment ) => Promise < void > ; contentFocusRef : MutableRefObject < ( ) => void > ; editorActionsRef : EditorActionsRef ; keydownHandler ? : ( e : KeyboardEvent ) => void ; } const SquireEditorWrapper = ( { message , disabled , breakpoints , onReady , onChange , onChangeContent , onAddAttachments , onRemoveAttachment , onFocus , contentFocusRef , editorActionsRef , keydownHandler = noop , } ) => { const [ mailSettings ] = useMailSettings ( ) ; const [ addresses ] = useAddresses ( ) ; const isMounted = useIsMounted ( ) ; const [ editorReady , setEditorReady ] = useState ( false ) ; const [ documentReady , setDocumentReady ] = useState ( false ) ; const [ blockquoteExpanded , setBlockquoteExpanded ] = useState ( true ) ; const [ blockquoteSaved , setBlockquoteSaved ] = useState < string > ( ) ; const skipNextInputRef = useRef ( false ) ; const [ cids , setCIDs ] = useState < string [ ] > ( [ ] ) ; const squireEditorRef = useRef < SquireEditorRef > ( null ) ; const isPlainText = testIsPlainText ( message . data ) ; const rightToLeft = message . data ?. RightToLeft ? RIGHT_TO_LEFT . ON : RIGHT_TO_LEFT . OFF ; const defaultFont = useMemo < FontData > ( ( ) => { const fontSettings = { FontFace : mailSettings ?. FontFace || DEFAULT_FONT_FACE , FontSize : mailSettings ?. FontSize || DEFAULT_FONT_SIZE , } ; return fontSettings ; } , [ mailSettings ] ) ; const metadata = useMemo ( ( ) => ( { supportPlainText : true , isPlainText , supportRightToLeft : true , rightToLeft , supportImages : true , } ) , [ isPlainText , rightToLeft ] ) ; useEffect ( ( ) => { if ( isPlainText && message . messageDocument ?. plainText !== undefined ) { setDocumentReady ( true ) ; } if ( ! isPlainText && message . messageDocument ?. document ?. innerHTML ) { setDocumentReady ( true ) ; } } , [ isPlainText , message . messageDocument ?. plainText , message . messageDocument ?. document ?. innerHTML ] ) ; const handleGetContent = useHandler ( ( ) => { const editorContent = squireEditorRef . current ?. value || '' ; if ( ! blockquoteExpanded && blockquoteSaved !== '' ) { return editorContent + blockquoteSaved ; } return editorContent ; } ) ; const checkImageDeletion = useHandler ( ( ) => { if ( ! isMounted ( ) ) { return ; } const newCIDs = findCIDsInContent ( handleGetContent ( ) ) ; const removedCIDs = diff ( cids , newCIDs ) ; if ( removedCIDs . length ) { removedCIDs . forEach ( ( cid ) => { const embeddedImages = getEmbeddedImages ( message ) ; const attachment = embeddedImages . find ( ( image ) => image . cid === cid ) ?. attachment ; if ( attachment ) { void onRemoveAttachment ( attachment ) ; } } ) ; squireEditorRef . current ?. clearUndoHistory ( ) ; } setCIDs ( newCIDs ) ; } , { debounce : 500 } ) ; const handleSetContent = ( message ) => { let content ; if ( isPlainText ) { content = getContent ( message ) ; } else { const [ contentBeforeBlockquote , blockquote ] = locateBlockquote ( message . messageDocument ?. document ) ; if ( blockquoteSaved === undefined ) { content = contentBeforeBlockquote ; setBlockquoteSaved ( blockquote ) ; setBlockquoteExpanded ( blockquote === '' ) ; } else { content = blockquoteExpanded ? contentBeforeBlockquote + blockquote : contentBeforeBlockquote ; } } skipNextInputRef . current = true ; if ( squireEditorRef . current ) { squireEditorRef . current . value = content ; } checkImageDeletion ( ) ; } ; useEffect ( ( ) => { if ( isPlainText ) { setEditorReady ( false ) ; } if ( documentReady && ( isPlainText || editorReady ) ) { handleSetContent ( message ) ; onReady ( ) ; } } , [ editorReady , documentReady , isPlainText ] ) ; useEffect ( ( ) => { if ( documentReady && ( isPlainText || ( editorReady && blockquoteSaved !== undefined ) ) ) { onChangeContent ( handleGetContent ( ) , false , true ) ; } } , [ documentReady , isPlainText , editorReady , blockquoteSaved !== undefined ] ) ; const handleInput = useCallback ( ( content ) => { if ( ! isPlainText ) { if ( skipNextInputRef . current ) { skipNextInputRef . current = false ; return ; } checkImageDeletion ( ) ; } if ( ! blockquoteExpanded ) { onChangeContent ( content + blockquoteSaved ) ; } else { onChangeContent ( content ) ; } } , [ onChangeContent , isPlainText , blockquoteExpanded ] ) ; const switchToPlainText = ( ) => { const MIMEType = MIME_TYPES . PLAINTEXT ; const plainText = exportPlainText ( handleGetContent ( ) ) ; const messageImages = message . messageImages ? { ... message . messageImages , images : [ ] } : undefined ; onChange ( { messageDocument : { plainText } , data : { MIMEType } , messageImages } ) ; } ; const switchToHTML = ( ) => { const MIMEType = MIME_TYPES . DEFAULT ; let content = plainTextToHTML ( message . data , message . messageDocument ?. plainText , mailSettings , addresses ) ; const fontStyle = defaultFontStyle ( mailSettings ) ; content = fontStyle ? `<div style="${ fontStyle }">${ content }</div>` : `<div>${ content }</div>` ; const document = setDocumentContent ( message . messageDocument ?. document , content ) ; onChange ( { messageDocument : { document } , data : { MIMEType } } ) ; } ; const handleChangeMetadata = useHandler ( ( change ) => { if ( change . isPlainText !== undefined ) { if ( change . isPlainText ) { switchToPlainText ( ) ; setBlockquoteExpanded ( true ) ; } else { switchToHTML ( ) ; } } if ( change . rightToLeft !== undefined ) { onChange ( { data : { RightToLeft : change . rightToLeft } } ) ; } } ) ; const handleInsertEmbedded = useHandler ( ( attachment , data ) => { const { cid } = readContentIDandLocation ( attachment ) ; const url = createBlob ( attachment , data ) ; squireEditorRef . current ?. insertImage ( url , { 'data-embedded-img' : `cid:${ cid }` , alt : attachment . Name , } ) ; } ) ; const handleRemoveEmbedded = useHandler ( async ( attachment ) => { const document = squireEditorRef . current ?. document ; if ( document ) { removeEmbeddedHTML ( document , attachment ) ; } } ) ; useEffect ( ( ) => { editorActionsRef . current = { getContent : handleGetContent , setContent : handleSetContent , insertEmbedded : handleInsertEmbedded , removeEmbedded : handleRemoveEmbedded , } ; contentFocusRef . current = ( ) => { squireEditorRef . current ?. focus ( ) ; } ; } , [ blockquoteExpanded , blockquoteSaved , isPlainText ] ) ; const handleEllipseClick = useHandler ( ( ) => { setBlockquoteExpanded ( true ) ; skipNextInputRef . current = true ; if ( squireEditorRef . current ) { const content = squireEditorRef . current . value ; squireEditorRef . current . value = content + blockquoteSaved ; } } ) ; const handleSquireReady = useCallback ( ( ) => setEditorReady ( true ) , [ ] ) ; return ( < SquireEditor ref = { squireEditorRef } placeholder = { c ( 'Placeholder' ) . t `Write your message` } metadata = { metadata } disabled = { disabled } isNarrow = { breakpoints . isNarrow } onChange = { handleInput } onChangeMetadata = { handleChangeMetadata } onFocus = { onFocus } showEllipseButton = { ! blockquoteExpanded } onEllipseClick = { handleEllipseClick } onReady = { handleSquireReady } onAddImages = { onAddAttachments } keydownHandler = { keydownHandler } defaultFont = { defaultFont } / > ) ; } ; export default memo ( SquireEditorWrapper ) ;