import { useEffect , useMemo , useRef , useState , memo , forwardRef , Ref , useImperativeHandle } from 'react' ; import * as React from 'react' ; import { hasAttachments , isDraft , isSent , isOutbox } from '@proton/shared/lib/mail/messages' ; import { Message } from '@proton/shared/lib/interfaces/mail/Message' ; import { classnames } from '@proton/components' ; import { Label } from '@proton/shared/lib/interfaces/Label' ; import { MailSettings } from '@proton/shared/lib/interfaces' ; import { noop } from '@proton/shared/lib/helpers/function' ; import createScrollIntoView from '@proton/components/helpers/createScrollIntoView' ; import { getSentStatusIconInfo , getReceivedStatusIcon , MessageViewIcons } from '../../helpers/message/icon' ; import MessageBody from './MessageBody' ; import HeaderCollapsed from './header/HeaderCollapsed' ; import HeaderExpanded from './header/HeaderExpanded' ; import MessageFooter from './MessageFooter' ; import { Element } from '../../models/element' ; import { useMessage } from '../../hooks/message/useMessage' ; import { useMarkAs , MARK_AS_STATUS } from '../../hooks/useMarkAs' ; import { isUnread } from '../../helpers/elements' ; import { Breakpoints } from '../../models/utils' ; import { useLoadMessage } from '../../hooks/message/useLoadMessage' ; import { useInitializeMessage } from '../../hooks/message/useInitializeMessage' ; import { useLoadEmbeddedImages , useLoadRemoteImages } from '../../hooks/message/useLoadImages' ; import { useResignContact } from '../../hooks/message/useResignContact' ; import { useVerifyMessage } from '../../hooks/message/useVerifyMessage' ; import { useMessageHotkeys } from '../../hooks/message/useMessageHotkeys' ; import { useEncryptedSearchContext } from '../../containers/EncryptedSearchProvider' ; import { isMessageForwarded } from '../../helpers/encryptedSearch/esBuild' ; import { useOnCompose } from '../../containers/ComposeProvider' ; import { LOAD_RETRY_COUNT } from '../../constants' ; import './MessageView.scss' ; interface Props { labelID : string ; conversationMode : boolean ; loading : boolean ; labels : Label [ ] ; message : Message ; mailSettings : MailSettings ; conversationIndex ? : number ; conversationID ? : string ; onBack : ( ) => void ; breakpoints : Breakpoints ; onFocus ? : ( index : number ) => void ; onMessageReady ? : ( ) => void ; columnLayout ? : boolean ; isComposerOpened : boolean ; containerRef ? : React . RefObject < HTMLElement > ; wrapperRef ? : React . RefObject < HTMLDivElement > ; highlightKeywords ? : boolean ; } export interface MessageViewRef { expand : ( ) => void ; } const OFFSET_PERCENTAGE = 0.25 ; const MessageView = ( { labelID , conversationMode , loading , labels = [ ] , message : inputMessage , mailSettings , conversationIndex = 0 , conversationID , onBack , breakpoints , onFocus = noop , onMessageReady , columnLayout = false , isComposerOpened , containerRef , wrapperRef , highlightKeywords = false , } , ref ) => { const getInitialExpand = ( ) => ! conversationMode && ! isDraft ( inputMessage ) && ! isOutbox ( inputMessage ) ; const [ expanded , setExpanded ] = useState ( getInitialExpand ) ; const { isSearchResult } = useEncryptedSearchContext ( ) ; const defaultOriginalMessageMode = isSearchResult ( inputMessage . ID ) && isMessageForwarded ( inputMessage . Subject ) ; const [ originalMessageMode , setOriginalMessageMode ] = useState ( defaultOriginalMessageMode ) ; const [ sourceMode , setSourceMode ] = useState ( false ) ; const elementRef = useRef < HTMLElement > ( null ) ; const { message , messageLoaded , bodyLoaded } = useMessage ( inputMessage . ID , conversationID ) ; const load = useLoadMessage ( inputMessage ) ; const initialize = useInitializeMessage ( message . localID , labelID ) ; const verify = useVerifyMessage ( message . localID ) ; const loadRemoteImages = useLoadRemoteImages ( message . localID ) ; const loadEmbeddedImages = useLoadEmbeddedImages ( message . localID ) ; const resignContact = useResignContact ( message . localID ) ; const markAs = useMarkAs ( ) ; const onCompose = useOnCompose ( ) ; const draft = ! loading && isDraft ( message . data ) ; const outbox = ! loading && ( isOutbox ( message . data ) || message . draftFlags ?. sending ) ; const sent = isSent ( message . data ) ; const unread = isUnread ( message . data , labelID ) ; const showFooter = hasAttachments ( message . data ) && Array . isArray ( message . data ?. Attachments ) ; const messageViewIcons = useMemo < MessageViewIcons > ( ( ) => { if ( sent ) { return getSentStatusIconInfo ( message ) ; } return { globalIcon : getReceivedStatusIcon ( message . data , message . verification ) , mapStatusIcon : { } } ; } , [ message ] ) ; const handleLoadRemoteImages = async ( ) => { await loadRemoteImages ( ) ; } ; const handleResignContact = async ( ) => { await resignContact ( ) ; } ; const handleLoadEmbeddedImages = async ( ) => { await loadEmbeddedImages ( ) ; } ; const handleToggle = ( value ) => ( ) => { if ( draft && ! outbox ) { onCompose ( { existingDraft : message , fromUndo : false } ) ; return ; } setExpanded ( value ) ; } ; const toggleOriginalMessage = ( ) => setOriginalMessageMode ( ! originalMessageMode ) ; const setParentBottomPadding = ( value ) => { if ( ! elementRef . current ) { return ; } const parent = elementRef . current . parentNode as HTMLElement ; parent . classList . add ( 'pb-custom' ) ; parent . style . setProperty ( '--padding-bottom-custom' , `${ value }px` ) ; } ; const resetParentBottomPadding = ( ) => { setParentBottomPadding ( 0 ) ; } ; const scrollToMessage = ( ) => { if ( ! elementRef . current || ! containerRef ?. current || ! wrapperRef ?. current ) { return ; } resetParentBottomPadding ( ) ; const { offsetHeight : containerHeight } = containerRef . current ; const header = containerRef . current . firstChild as HTMLElement ; const { offsetHeight : headerHeight } = header ; const offset = ( containerHeight - headerHeight ) * OFFSET_PERCENTAGE ; const totalOffset = headerHeight + offset ; if ( elementRef . current . offsetTop - totalOffset < containerRef . current . scrollTop ) { return ; } const wrapperPaddingBottom = parseInt ( getComputedStyle ( wrapperRef . current ) . paddingBottom , 10 ) ; const wrapperPaddingTop = parseInt ( getComputedStyle ( wrapperRef . current ) . paddingTop , 10 ) ; setParentBottomPadding ( Math . max ( containerHeight - totalOffset - elementRef . current . offsetHeight + wrapperPaddingBottom + wrapperPaddingTop , 0 ) ) ; createScrollIntoView ( elementRef . current , containerRef . current , false , totalOffset ) ; } ; useImperativeHandle ( ref , ( ) => ( { expand : ( ) => { if ( ! isDraft ( message . data ) ) { setExpanded ( true ) ; if ( ! columnLayout ) { elementRef . current ?. parentElement ?. focus ( ) ; } } } , } ) ) ; useEffect ( ( ) => { if ( ! loading && ! messageLoaded ) { void load ( ) ; } if ( ! isComposerOpened && isDraft ( message . data ) && messageLoaded ) { if ( onMessageReady ) { setTimeout ( onMessageReady ) ; } if ( ! columnLayout ) { elementRef . current ?. parentElement ?. focus ( ) ; } } } , [ loading , messageLoaded , bodyLoaded , message . data ?. ID ] ) ; useEffect ( ( ) => { if ( ! loading && expanded && message . messageDocument ?. initialized === undefined ) { if ( ( message . loadRetry || 0 ) > LOAD_RETRY_COUNT ) { return ; } void initialize ( ) ; } } , [ loading , expanded , message . messageDocument ?. initialized ] ) ; useEffect ( ( ) => { if ( ! loading && expanded && message . messageDocument ?. initialized && message . data && message . verification === undefined ) { void verify ( message . decryption ?. decryptedRawContent , message . decryption ?. signature ) ; } } , [ loading , expanded , message . messageDocument ?. initialized , message . verification ] ) ; useEffect ( ( ) => { if ( expanded ) { scrollToMessage ( ) ; } } , [ expanded ] ) ; useEffect ( ( ) => { if ( expanded && unread && bodyLoaded ) { markAs ( [ message . data as Element ] , labelID , MARK_AS_STATUS . READ ) ; } } , [ expanded , unread , bodyLoaded ] ) ; useEffect ( ( ) => { if ( message . data ?. ID ) { setExpanded ( getInitialExpand ) ; setSourceMode ( false ) ; } } , [ message . data ?. ID ] ) ; const hasProcessingErrors = ! ! message . errors ?. processing ?. length ; useEffect ( ( ) => { if ( hasProcessingErrors ) { setSourceMode ( true ) ; } } , [ hasProcessingErrors ] ) ; const { hasFocus , handleFocus , handleBlur , labelDropdownToggleRef , moveDropdownToggleRef , filterDropdownToggleRef , } = useMessageHotkeys ( elementRef , { labelID , conversationIndex , message , bodyLoaded , expanded , messageLoaded , draft , conversationMode , mailSettings , messageRef : elementRef , } , { onFocus , setExpanded , toggleOriginalMessage , handleLoadRemoteImages , handleLoadEmbeddedImages , onBack , } ) ; return ( < article ref = { elementRef } className = { classnames ( [ 'message-container m0-5 mb1 no-outline' , expanded && 'is-opened' , hasAttachments ( message . data ) && 'message-container--hasAttachment' , hasFocus && 'is-focused' , ] ) } style = { { '--index' : conversationIndex * 2 } } data - testid = "message-view" tabIndex = { - 1 } data - message - id = { message . data ?. ID } data - shortcut - target = "message-container" onFocus = { handleFocus } onBlur = { handleBlur } > { expanded ? ( < > < HeaderExpanded labelID = { labelID } conversationMode = { conversationMode } message = { message } messageViewIcons = { messageViewIcons } messageLoaded = { messageLoaded } bodyLoaded = { bodyLoaded } isSentMessage = { sent } sourceMode = { sourceMode } onLoadRemoteImages = { handleLoadRemoteImages } onLoadEmbeddedImages = { handleLoadEmbeddedImages } onResignContact = { handleResignContact } labels = { labels } mailSettings = { mailSettings } onToggle = { handleToggle ( false ) } onBack = { onBack } onSourceMode = { setSourceMode } breakpoints = { breakpoints } labelDropdownToggleRef = { labelDropdownToggleRef } moveDropdownToggleRef = { moveDropdownToggleRef } filterDropdownToggleRef = { filterDropdownToggleRef } highlightKeywords = { highlightKeywords } parentMessageRef = { elementRef } / > < MessageBody labelID = { labelID } messageLoaded = { messageLoaded } bodyLoaded = { bodyLoaded } sourceMode = { sourceMode } message = { message } originalMessageMode = { originalMessageMode } toggleOriginalMessage = { toggleOriginalMessage } onMessageReady = { onMessageReady } highlightKeywords = { highlightKeywords } / > { showFooter ? < MessageFooter message = { message } / > : null } < />) : (<HeaderCollapsed labelID={labelID} labels={labels} message={message} messageLoaded={messageLoaded} isSentMessage={sent} isUnreadMessage={unread} onExpand={handleToggle(true)} breakpoints={breakpoints} highlightKeywords={highlightKeywords}/ > ) } < /article>); } ; export default memo ( forwardRef ( MessageView ) ) ;