import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ; import { Chat , ChatItemProps , Flex , Ref , ShorthandValue } from '@fluentui/react-northstar' ; import { DownIconStyle , newMessageButtonContainerStyle , messageThreadContainerStyle , chatStyle , buttonWithIconStyles , newMessageButtonStyle , messageStatusContainerStyle , noMessageStatusStyle , defaultChatItemMessageContainer , defaultMyChatMessageContainer , defaultChatMessageContainer , gutterWithAvatar , gutterWithHiddenAvatar } from './styles/MessageThread.styles' ; import { Icon , IStyle , mergeStyles , Persona , PersonaSize , PrimaryButton , Stack , IPersona } from '@fluentui/react' ; import { ComponentSlotStyle } from '@fluentui/react-northstar' ; import { LiveAnnouncer } from 'react-aria-live' ; import { delay } from './utils/delay' ; import { BaseCustomStyles , ChatMessage , CustomMessage , SystemMessage , CommunicationParticipant , OnRenderAvatarCallback , ParticipantAddedSystemMessage , ParticipantRemovedSystemMessage , Message } from '../types' ; import { MessageStatusIndicator , MessageStatusIndicatorProps } from './MessageStatusIndicator' ; import { memoizeFnAll , MessageStatus } from '@internal/acs-ui-common' ; import { SystemMessage as SystemMessageComponent , SystemMessageIconTypes } from './SystemMessage' ; import { ChatMessageComponent } from './ChatMessage/ChatMessageComponent' ; import { useLocale } from '../localization/LocalizationProvider' ; import { isNarrowWidth , useContainerWidth } from './utils/responsive' ; const isMessageSame = ( first , second ) : boolean => { return ( first . messageId === second . messageId && first . content === second . content && first . contentType === second . contentType && JSON . stringify ( first . createdOn ) === JSON . stringify ( second . createdOn ) && first . senderId === second . senderId && first . senderDisplayName === second . senderDisplayName && first . status === second . status ) ; } ; param messages const getLatestChatMessage = ( messages ) : ChatMessage | undefined => { for ( let i = messages . length - 1 ; i >= 0 ; i -- ) { const message = messages [ i ] ; if ( message . messageType === 'chat' && ! ! message . createdOn ) { return message ; } } return undefined ; } ; /**
 * Compare latestMessageFromPreviousMessages & latestMessageFromNewMessages to see if the new message is not from
 * current user.
 */ const isThereNewMessageNotFromCurrentUser = ( userId , latestMessageFromPreviousMessages ? , latestMessageFromNewMessages ? ) : boolean => { if ( latestMessageFromNewMessages === undefined ) { return false ; } if ( latestMessageFromPreviousMessages === undefined ) { return latestMessageFromNewMessages . senderId !== userId ; } return ( ! isMessageSame ( latestMessageFromNewMessages , latestMessageFromPreviousMessages ) && latestMessageFromNewMessages . senderId !== userId ) ; } ; /**
 * Returns true if the current user sent the latest message and false otherwise. It will ignore messages that have no
 * sender, messages that have failed to send, and messages from the current user that is marked as SEEN. This is meant
 * as an indirect way to detect if user is at bottom of the chat when the component updates with new messages. If we
 * updated this component due to current user sending a message we want to then call scrollToBottom.
 */ const didUserSendTheLatestMessage = ( userId , latestMessageFromPreviousMessages ? , latestMessageFromNewMessages ? ) : boolean => { if ( latestMessageFromNewMessages === undefined ) { return false ; } if ( latestMessageFromPreviousMessages === undefined ) { return latestMessageFromNewMessages . senderId === userId ; } return ( ! isMessageSame ( latestMessageFromNewMessages , latestMessageFromPreviousMessages ) && latestMessageFromNewMessages . senderId === userId ) ; } ; /**
 * Fluent styles for  MessageThread .
 *
 *  public export interface MessageThreadStyles extends BaseCustomStyles { /** Styles for load previous messages container. */ loadPreviousMessagesButtonContainer ? : IStyle ; /** Styles for new message container. */ newMessageButtonContainer ? : IStyle ; /** Styles for chat container. */ chatContainer ? : ComponentSlotStyle ; /** styles for my chat items.  */ myChatItemMessageContainer ? : ComponentSlotStyle ; /** styles for chat items.  */ chatItemMessageContainer ? : ComponentSlotStyle ; /** Styles for my chat message container. */ myChatMessageContainer ? : ComponentSlotStyle ; /** Styles for chat message container. */ chatMessageContainer ? : ComponentSlotStyle ; /** Styles for system message container. */ systemMessageContainer ? : ComponentSlotStyle ; /** Styles for message status indicator container. */ messageStatusContainer ? : ( mine : boolean ) => IStyle ; } /**
 * Strings of  MessageThread that can be overridden.
 *
 *  public export interface MessageThreadStrings { /** String for Sunday */ sunday : string ; /** String for Monday */ monday : string ; /** String for Tuesday */ tuesday : string ; /** String for Wednesday */ wednesday : string ; /** String for Thursday */ thursday : string ; /** String for Friday */ friday : string ; /** String for Saturday */ saturday : string ; /** String for Yesterday */ yesterday : string ; /** String for participants joined */ participantJoined : string ; /** String for participants left */ participantLeft : string ; /** Tag shown on a message that has been edited */ editedTag : string ; /** String for editing message in floating menu */ editMessage : string ; /** String for removing message in floating menu */ removeMessage : string ; /** String for LiveMessage introduction for the Chat Message */ liveAuthorIntro : string ; /** String for warning on text limit exceeded in EditBox*/ editBoxTextLimit : string ; /** String for placeholder text in EditBox when there is no user input*/ editBoxPlaceholderText : string ; /** String for new messages indicator*/ newMessagesIndicator : string ; /** String for replacing display name when there is none*/ noDisplayNameSub : string ; /** String for Cancel button in EditBox*/ editBoxCancelButton : string ; /** String for Submit in EditBox when there is no user input*/ editBoxSubmitButton : string ; } /**
 * Arguments for  MessageThreadProps . onRenderJumpToNewMessageButton .
 *
 *  public export interface JumpToNewMessageButtonProps { /** String for button text */ text : string ; /** Callback for when button is clicked */ onClick : ( ) => void ; } const DefaultJumpToNewMessageButton = ( props ) : JSX . Element => { const { text , onClick } = props ; return ( < PrimaryButton className = { newMessageButtonStyle } styles = { buttonWithIconStyles } text = { text } onClick = { onClick } onRenderIcon = { ( ) => < Icon iconName = "Down" className = { DownIconStyle } / > } />); } ; const generateParticipantsStr = ( participants , defaultName ) : string => participants . map ( ( participant ) => `${ ! participant . displayName || participant . displayName === '' ? defaultName : participant . displayName }` ) . join ( ', ' ) ; public export type MessageRenderer = ( props : MessageProps ) => JSX . Element ; const ParticipantSystemMessageComponent = ( { message , style , defaultName } ) : JSX . Element => { const { strings } = useLocale ( ) ; const participantsStr = generateParticipantsStr ( message . participants , defaultName ) ; const messageSuffix = message . systemMessageType === 'participantAdded' ? strings . messageThread . participantJoined : strings . messageThread . participantLeft ; if ( participantsStr !== '' ) { return ( < SystemMessageComponent iconName = { ( message . iconName ? message . iconName : '' ) as SystemMessageIconTypes } content = { `${ participantsStr } ${ messageSuffix }` } containerStyle = { style } / > ) ; } return < > < />; } ; const DefaultSystemMessage = ( props ) => { const message = props . message ; switch ( message . messageType ) { case 'system' : switch ( message . systemMessageType ) { case 'content' : return ( < SystemMessageComponent iconName = { ( message . iconName ? message . iconName : '' ) as SystemMessageIconTypes } content = { message . content ?? '' } containerStyle = { props ?. messageContainerStyle } / > ) ; case 'participantAdded' : case 'participantRemoved' : return ( < ParticipantSystemMessageComponent message = { message } style = { props . messageContainerStyle } defaultName = { props . strings . noDisplayNameSub } / > ) ; } } return < > < />; } ; const memoizeAllMessages = memoizeFnAll ( ( _messageKey , message , showMessageDate , showMessageStatus , onRenderAvatar , shouldOverlapAvatarAndMessage , styles , onRenderMessageStatus , defaultStatusRenderer , defaultChatMessageRenderer , strings , _attached ? , statusToRender ? , onRenderMessage ? , onUpdateMessage ? , onDeleteMessage ? ) : ShorthandValue < ChatItemProps > => { const messageProps = { message , strings , showDate : showMessageDate , onUpdateMessage , onDeleteMessage } ; switch ( message . messageType ) { case 'chat' : { const myChatMessageStyle = styles ?. myChatMessageContainer || defaultMyChatMessageContainer ; const chatMessageStyle = styles ?. chatMessageContainer || defaultChatMessageContainer ; messageProps . messageContainerStyle = message . mine ? myChatMessageStyle : chatMessageStyle ; const chatMessageComponent = onRenderMessage === undefined ? defaultChatMessageRenderer ( messageProps ) : onRenderMessage ( messageProps , defaultChatMessageRenderer ) ; const personaOptions = { hidePersonalDetails : true , size : PersonaSize . size32 , text : message . senderDisplayName } ; const chatItemMessageStyle = ( message . mine ? styles ?. myChatItemMessageContainer : styles ?. chatItemMessageContainer ) || defaultChatItemMessageContainer ( shouldOverlapAvatarAndMessage ) ; const chatGutterStyles = message . attached === 'top' || message . attached === false ? gutterWithAvatar : gutterWithHiddenAvatar ; return { gutter : { styles : chatGutterStyles , content : message . mine ? ( '' ) : onRenderAvatar ? ( onRenderAvatar ( message . senderId ?? '' , personaOptions ) ) : ( < Persona { ... personaOptions } / > ) } , contentPosition : message . mine ? 'end' : 'start' , message : { styles : chatItemMessageStyle , content : ( < Flex hAlign = { message . mine ? 'end' : undefined } vAlign = "end" > { chatMessageComponent } < div className = { mergeStyles ( messageStatusContainerStyle ( message . mine ?? false ) , styles ?. messageStatusContainer ? styles . messageStatusContainer ( message . mine ?? false ) : '' ) } > { showMessageStatus && statusToRender ? ( onRenderMessageStatus ? ( onRenderMessageStatus ( { status : statusToRender } ) ) : ( defaultStatusRenderer ( statusToRender ) ) ) : ( < div className = { mergeStyles ( noMessageStatusStyle ) } / > ) } < /div> < /Flex>) } , attached : message . attached , key : _messageKey } ; } case 'system' : { messageProps . messageContainerStyle = styles ?. systemMessageContainer ; const systemMessageComponent = onRenderMessage === undefined ? ( < DefaultSystemMessage { ... messageProps } / > ) : ( onRenderMessage ( messageProps , ( props ) => < DefaultSystemMessage { ... props } / > ) ) ; return { children : systemMessageComponent , key : _messageKey } ; } default : { const customMessageComponent = onRenderMessage === undefined ? < > < /> : onRenderMessage(messageProps); return { children : customMessageComponent , key : _messageKey } ; } } } ) ; const getLastChatMessageIdWithStatus = ( messages , status ) : string | undefined => { for ( let i = messages . length - 1 ; i >= 0 ; i -- ) { const message = messages [ i ] ; if ( message . messageType === 'chat' && message . status === status && message . mine ) { return message . messageId ; } } return undefined ; } ; /**
 * Props for  MessageThread .
 *
 *  public export type MessageThreadProps = { /**
     * UserId of the current user.
     */ userId : string ; /**
     * Messages to render in message thread. A message can be of type `ChatMessage`, `SystemMessage` or `CustomMessage`.
     */ messages : ( ChatMessage | SystemMessage | CustomMessage ) [ ] ; Example styles ? : MessageThreadStyles ; defaultValue disableJumpToNewMessageButton ? : boolean ; defaultValue showMessageDate ? : boolean ; defaultValue showMessageStatus ? : boolean ; defaultValue numberOfChatMessagesToReload ? : number ; param messageId onMessageSeen ? : ( messageId : string ) => Promise < void > ; param messageStatusIndicatorProps onRenderMessageStatus ? : ( messageStatusIndicatorProps : MessageStatusIndicatorProps ) => JSX . Element | null ; param userId onRenderAvatar ? : OnRenderAvatarCallback ; param newMessageButtonProps onRenderJumpToNewMessageButton ? : ( newMessageButtonProps : JumpToNewMessageButtonProps ) => JSX . Element ; /**
     * Optional callback to override loading of previous messages.
     * It accepts the number of history chat messages that we want to load and return a boolean Promise indicating if we have got all the history messages.
     * If the promise resolves to `true`, we have load all chat messages into the message thread and `loadPreviousMessagesButton` will not be rendered anymore.
     */ onLoadPreviousChatMessages ? : ( messagesToLoad : number ) => Promise < boolean > ; param messageProps - props of type  communication-react param defaultOnRender - default render of type  communication-react remarks onRenderMessage ? : ( messageProps : MessageProps , messageRenderer ? : MessageRenderer ) => JSX . Element ; param messageId param content onUpdateMessage ? : ( messageId : string , content : string ) => Promise < void > ; param messageId onDeleteMessage ? : ( messageId : string ) => Promise < void > ; remarks defaultValue disableEditing ? : boolean ; /**
     * Optional strings to override in component
     */ strings ? : Partial < MessageThreadStrings > ; } ; /**
 * Props to render a single message.
 *
 * See  MessageRenderer .
 *
 *  public export type MessageProps = { /**
     * Message to render. It can type `ChatMessage` or `SystemMessage` or `CustomMessage`.
     */ message : Message ; /**
     * Strings from parent MessageThread component
     */ strings : MessageThreadStrings ; /**
     * Custom CSS styles for chat message container.
     */ messageContainerStyle ? : ComponentSlotStyle ; defaultValue showDate ? : boolean ; remarks defaultValue disableEditing ? : boolean ; param messageId param content onUpdateMessage ? : ( messageId : string , content : string ) => Promise < void > ; param messageId onDeleteMessage ? : ( messageId : string ) => Promise < void > ; } ; param props public export const MessageThread = ( props ) : JSX . Element => { const { messages : newMessages , userId , styles , disableJumpToNewMessageButton = false , showMessageDate = false , showMessageStatus = false , numberOfChatMessagesToReload = 5 , onMessageSeen , onRenderMessageStatus , onRenderAvatar , onLoadPreviousChatMessages , onRenderJumpToNewMessageButton , onRenderMessage , onUpdateMessage , onDeleteMessage } = props ; const [ messages , setMessages ] = useState < ( ChatMessage | SystemMessage | CustomMessage ) [ ] > ( [ ] ) ; const [ chatMessagesInitialized , setChatMessagesInitialized ] = useState < boolean > ( false ) ; const [ isAtBottomOfScroll , setIsAtBottomOfScroll ] = useState < boolean > ( true ) ; const [ forceUpdate , setForceUpdate ] = useState < number > ( 0 ) ; const [ latestPreviousChatMessage , setLatestPreviousChatMessage ] = useState < ChatMessage | undefined > ( undefined ) ; const [ latestCurrentChatMessage , setLatestCurrentChatMessage ] = useState < ChatMessage | undefined > ( undefined ) ; const [ existsNewChatMessage , setExistsNewChatMessage ] = useState < boolean > ( false ) ; const [ lastSeenChatMessage , setLastSeenChatMessage ] = useState < string | undefined > ( undefined ) ; const [ lastDeliveredChatMessage , setLastDeliveredChatMessage ] = useState < string | undefined > ( undefined ) ; const [ lastSendingChatMessage , setLastSendingChatMessage ] = useState < string | undefined > ( undefined ) ; const isAllChatMessagesLoadedRef = useRef ( false ) ; const previousTopRef = useRef < number > ( - 1 ) ; const previousHeightRef = useRef < number > ( - 1 ) ; const messageIdSeenByMeRef = useRef < string > ( '' ) ; const chatScrollDivRef = useRef < HTMLElement > ( null ) ; const chatThreadRef = useRef < HTMLElement > ( null ) ; const isLoadingChatMessagesRef = useRef ( false ) ; const chatThreadWidth = useContainerWidth ( chatThreadRef ) ; const isNarrow = isNarrowWidth ( chatThreadWidth ) ; const messagesRef = useRef ( messages ) ; const setMessagesRef = ( messagesWithAttachedValue ) : void => { messagesRef . current = messagesWithAttachedValue ; setMessages ( messagesWithAttachedValue ) ; } ; const isAtBottomOfScrollRef = useRef ( isAtBottomOfScroll ) ; const setIsAtBottomOfScrollRef = ( isAtBottomOfScrollValue ) : void => { isAtBottomOfScrollRef . current = isAtBottomOfScrollValue ; setIsAtBottomOfScroll ( isAtBottomOfScrollValue ) ; } ; const chatMessagesInitializedRef = useRef ( chatMessagesInitialized ) ; const setChatMessagesInitializedRef = ( chatMessagesInitialized ) : void => { chatMessagesInitializedRef . current = chatMessagesInitialized ; setChatMessagesInitialized ( chatMessagesInitialized ) ; } ; const sendMessageStatusIfAtBottom = useCallback ( async ( ) : Promise < void > => { if ( ! isAtBottomOfScrollRef . current || ! document . hasFocus ( ) || ! messagesRef . current || messagesRef . current . length === 0 || ! showMessageStatus ) { return ; } const messagesWithId = messagesRef . current . filter ( ( message ) => { return message . messageType === 'chat' && ! message . mine && ! ! message . messageId ; } ) ; if ( messagesWithId . length === 0 ) { return ; } const lastMessage = messagesWithId [ messagesWithId . length - 1 ] as ChatMessage ; try { if ( onMessageSeen && lastMessage && lastMessage . messageId && lastMessage . messageId !== messageIdSeenByMeRef . current ) { await onMessageSeen ( lastMessage . messageId ) ; messageIdSeenByMeRef . current = lastMessage . messageId ; } } catch ( e ) { console . log ( 'onMessageSeen Error' , lastMessage , e ) ; } } , [ showMessageStatus , onMessageSeen ] ) ; const scrollToBottom = useCallback ( ( ) : void => { if ( chatScrollDivRef . current ) { chatScrollDivRef . current . scrollTop = chatScrollDivRef . current . scrollHeight ; } setExistsNewChatMessage ( false ) ; setIsAtBottomOfScrollRef ( true ) ; sendMessageStatusIfAtBottom ( ) ; } , [ sendMessageStatusIfAtBottom ] ) ; const handleScrollToTheBottom = useCallback ( ( ) : void => { if ( ! chatScrollDivRef . current ) { return ; } const atBottom = Math . ceil ( chatScrollDivRef . current . scrollTop ) >= chatScrollDivRef . current . scrollHeight - chatScrollDivRef . current . clientHeight ; if ( atBottom ) { sendMessageStatusIfAtBottom ( ) ; if ( ! isAtBottomOfScrollRef . current ) { scrollToBottom ( ) ; } } setIsAtBottomOfScrollRef ( atBottom ) ; } , [ scrollToBottom , sendMessageStatusIfAtBottom ] ) ; const fetchNewMessageWhenAtTop = useCallback ( async ( ) => { if ( chatScrollDivRef . current && ! isLoadingChatMessagesRef . current ) { if ( onLoadPreviousChatMessages ) { isLoadingChatMessagesRef . current = true ; while ( ! isAllChatMessagesLoadedRef . current && chatScrollDivRef . current . scrollTop <= 500 ) { isAllChatMessagesLoadedRef . current = await onLoadPreviousChatMessages ( numberOfChatMessagesToReload ) ; await delay ( 200 ) ; } isLoadingChatMessagesRef . current = false ; } } } , [ numberOfChatMessagesToReload , onLoadPreviousChatMessages ] ) ; const handleInfiniteScroll = useCallback ( ( ) : void => { if ( ! chatScrollDivRef . current ) { return ; } fetchNewMessageWhenAtTop ( ) ; } , [ fetchNewMessageWhenAtTop ] ) ; useEffect ( ( ) => { if ( ! chatScrollDivRef . current ) { return ; } previousTopRef . current = chatScrollDivRef . current . scrollTop ; previousHeightRef . current = chatScrollDivRef . current . scrollHeight ; } , [ newMessages ] ) ; useEffect ( ( ) => { if ( ! chatScrollDivRef . current ) { return ; } if ( previousTopRef . current === 0 ) { const currentHeight = chatScrollDivRef . current . scrollHeight ; chatScrollDivRef . current . scrollTop = chatScrollDivRef . current . scrollTop + currentHeight - previousHeightRef . current ; } } , [ messages ] ) ; useEffect ( ( ) => { fetchNewMessageWhenAtTop ( ) ; } , [ fetchNewMessageWhenAtTop ] ) ; /**
     * One time run useEffects. Sets up listeners when component is mounted and tears down listeners when component
     * unmounts unless these function changed
     */ useEffect ( ( ) => { window && window . addEventListener ( 'click' , sendMessageStatusIfAtBottom ) ; window && window . addEventListener ( 'focus' , sendMessageStatusIfAtBottom ) ; return ( ) => { window && window . removeEventListener ( 'click' , sendMessageStatusIfAtBottom ) ; window && window . removeEventListener ( 'focus' , sendMessageStatusIfAtBottom ) ; } ; } , [ sendMessageStatusIfAtBottom ] ) ; useEffect ( ( ) => { const chatScrollDiv = chatScrollDivRef . current ; chatScrollDiv ?. addEventListener ( 'scroll' , handleScrollToTheBottom ) ; chatScrollDiv ?. addEventListener ( 'scroll' , handleInfiniteScroll ) ; return ( ) => { chatScrollDiv ?. removeEventListener ( 'scroll' , handleScrollToTheBottom ) ; chatScrollDiv ?. removeEventListener ( 'scroll' , handleInfiniteScroll ) ; } ; } , [ handleInfiniteScroll , handleScrollToTheBottom ] ) ; /**
     * ClientHeight controls the number of messages to render. However ClientHeight will not be initialized after the
     * first render (not sure but I guess Fluent is updating it in hook which is after render maybe?) so we need to
     * trigger a re-render until ClientHeight is initialized. This force re-render should only happen once.
     */ const clientHeight = chatThreadRef . current ?. clientHeight ; useEffect ( ( ) => { if ( clientHeight === undefined ) { setForceUpdate ( forceUpdate + 1 ) ; return ; } isAtBottomOfScrollRef . current && scrollToBottom ( ) ; } , [ clientHeight , forceUpdate , scrollToBottom , chatMessagesInitialized ] ) ; /**
     * This needs to run to update latestPreviousChatMessage & latestCurrentChatMessage.
     * These two states are used to manipulate scrollbar
     */ useEffect ( ( ) => { setLatestPreviousChatMessage ( getLatestChatMessage ( messagesRef . current ) ) ; setLatestCurrentChatMessage ( getLatestChatMessage ( newMessages ) ) ; setMessagesRef ( newMessages ) ; ! chatMessagesInitializedRef . current && setChatMessagesInitializedRef ( true ) ; setLastDeliveredChatMessage ( getLastChatMessageIdWithStatus ( newMessages , 'delivered' ) ) ; setLastSeenChatMessage ( getLastChatMessageIdWithStatus ( newMessages , 'seen' ) ) ; setLastSendingChatMessage ( getLastChatMessageIdWithStatus ( newMessages , 'sending' ) ) ; } , [ newMessages ] ) ; /**
     * This needs to run after messages are rendererd so we can manipulate the scroll bar.
     */ useEffect ( ( ) => { if ( isThereNewMessageNotFromCurrentUser ( userId , latestPreviousChatMessage , latestCurrentChatMessage ) && ! isAtBottomOfScrollRef . current ) { setExistsNewChatMessage ( true ) ; } else if ( didUserSendTheLatestMessage ( userId , latestPreviousChatMessage , latestCurrentChatMessage ) || isAtBottomOfScrollRef . current ) { scrollToBottom ( ) ; } } , [ messages ] ) ; const defaultChatMessageRenderer = useCallback ( ( messageProps ) => { if ( messageProps . message . messageType === 'chat' ) { return ( < ChatMessageComponent { ... messageProps } message = { messageProps . message } inlineAcceptRejectEditButtons = { ! isNarrow } />); } return < > < />; } , [ new Date ( ) . toDateString ( ) , isNarrow ] ) ; const localeStrings = useLocale ( ) . strings . messageThread ; const strings = useMemo ( ( ) => ( { ... localeStrings , ... props . strings } ) , [ localeStrings , props . strings ] ) ; const defaultStatusRenderer = useCallback ( ( status ) => < MessageStatusIndicator status = { status } / > , [ ] ) ; const messagesToDisplay = useMemo ( ( ) => memoizeAllMessages ( ( memoizedMessageFn ) => { return messages . map ( ( message , index ) : ShorthandValue < ChatItemProps > => { let key = message . messageId ; let statusToRender = undefined ; if ( message . messageType === 'chat' ) { if ( ! message . messageId || message . messageId === '' ) { key = message . clientMessageId ; } if ( showMessageStatus && message . mine ) { switch ( message . messageId ) { case lastSeenChatMessage : { statusToRender = 'seen' ; break ; } case lastSendingChatMessage : { statusToRender = 'sending' ; break ; } case lastDeliveredChatMessage : { statusToRender = 'delivered' ; break ; } } } if ( message . mine && message . status === 'failed' ) { statusToRender = 'failed' ; } } return memoizedMessageFn ( key ?? 'id_' + index , message , showMessageDate , showMessageStatus , onRenderAvatar , isNarrow , styles , onRenderMessageStatus , defaultStatusRenderer , defaultChatMessageRenderer , strings , message . messageType === 'chat' ? message . attached : undefined , statusToRender , onRenderMessage , onUpdateMessage , onDeleteMessage ) ; } ) ; } ) , [ messages , showMessageDate , showMessageStatus , onRenderAvatar , isNarrow , styles , onRenderMessageStatus , defaultStatusRenderer , defaultChatMessageRenderer , lastSeenChatMessage , lastSendingChatMessage , lastDeliveredChatMessage , onRenderMessage , onUpdateMessage , onDeleteMessage , strings ] ) ; const chatBody = useMemo ( ( ) => { return ( < LiveAnnouncer > < Chat styles = { styles ?. chatContainer ?? chatStyle } items = { messagesToDisplay } / > < /LiveAnnouncer>); } , [ styles ?. chatContainer , messagesToDisplay ] ) ; return ( < Ref innerRef = { chatThreadRef } > < Stack className = { mergeStyles ( messageThreadContainerStyle , styles ?. root ) } grow > < Ref innerRef = { chatScrollDivRef } > { chatBody } < /Ref> { existsNewChatMessage && ! disableJumpToNewMessageButton && ( < div className = { mergeStyles ( newMessageButtonContainerStyle , styles ?. newMessageButtonContainer ) } > { onRenderJumpToNewMessageButton ? ( onRenderJumpToNewMessageButton ( { text : strings . newMessagesIndicator , onClick : scrollToBottom } ) ) : ( < DefaultJumpToNewMessageButton text = { strings . newMessagesIndicator } onClick = { scrollToBottom } / > ) } < /div>)} < /Stack> < /Ref>); } ;