import EventEmitter from 'events' ; import produce from 'immer' ; import { ChatClientState , ChatErrors , ChatThreadClientState , ChatThreadProperties , ChatErrorTarget , ChatError } from './ChatClientState' ; import { ChatMessageWithStatus } from './types/ChatMessageWithStatus' ; import { enableMapSet } from 'immer' ; import { ChatMessageReadReceipt , ChatParticipant } from '@azure/communication-chat' ; import { CommunicationIdentifierKind , UnknownIdentifierKind } from '@azure/communication-common' ; import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common' ; import { Constants } from './Constants' ; import { TypingIndicatorReceivedEvent } from '@azure/communication-signaling' ; import { ChatStateModifier } from './StatefulChatClient' ; enableMapSet ( ) ; private export class ChatContext { private _state : ChatClientState = { userId : { id : '' } as UnknownIdentifierKind , displayName : '' , threads : { } , latestErrors : { } as ChatErrors } ; private _batchMode = false ; private _emitter : EventEmitter ; private typingIndicatorInterval : number | undefined = undefined ; constructor ( maxListeners ? ) { this . _emitter = new EventEmitter ( ) ; if ( maxListeners ) { this . _emitter . setMaxListeners ( maxListeners ) ; } } public setState ( state ) { this . _state = state ; if ( ! this . _batchMode ) { this . _emitter . emit ( 'stateChanged' , this . _state ) ; } } public getState ( ) { return this . _state ; } public modifyState ( modifier ) { this . batch ( ( ) => { this . setState ( produce ( this . _state , ( draft ) => { modifier ( draft ) ; } ) ) ; } ) ; } public setThread ( threadId , threadState ) { this . setState ( produce ( this . _state , ( draft ) => { draft . threads [ threadId ] = threadState ; } ) ) ; } public createThread ( threadId , properties ? ) { this . setState ( produce ( this . _state , ( draft ) => { draft . threads [ threadId ] = { chatMessages : { } , threadId : threadId , properties : properties , participants : { } , readReceipts : [ ] , typingIndicators : [ ] , latestReadTime : new Date ( 0 ) } ; } ) ) ; } public updateChatConfig ( userId , displayName ) { this . setState ( produce ( this . _state , ( draft ) => { draft . displayName = displayName ; draft . userId = userId ; } ) ) ; } public createThreadIfNotExist ( threadId , properties ? ) { const exists = Object . prototype . hasOwnProperty . call ( this . getState ( ) . threads , threadId ) ; if ( ! exists ) { this . createThread ( threadId , properties ) ; return true ; } return false ; } public updateThread ( threadId , properties ? ) { this . setState ( produce ( this . _state , ( draft ) => { const thread = draft . threads [ threadId ] ; if ( thread ) { thread . properties = properties ; } } ) ) ; } public updateThreadTopic ( threadId , topic ? ) { this . setState ( produce ( this . _state , ( draft ) => { if ( topic === undefined ) { return ; } const thread = draft . threads [ threadId ] ; if ( thread && ! thread . properties ) { thread . properties = { topic : topic } ; } else if ( thread && thread . properties ) { thread . properties . topic = topic ; } } ) ) ; } public deleteThread ( threadId ) { this . setState ( produce ( this . _state , ( draft ) => { const thread = draft . threads [ threadId ] ; if ( thread ) { delete draft . threads [ threadId ] ; } } ) ) ; } public setChatMessages ( threadId , messages ) { this . setState ( produce ( this . _state , ( draft ) => { const threadState = draft . threads [ threadId ] ; if ( threadState ) { threadState . chatMessages = messages ; } const thread = draft . threads [ threadId ] ; if ( thread ) { for ( const message of Object . values ( messages ) ) { this . filterTypingIndicatorForUser ( thread , message . sender ) ; } } } ) ) ; } public updateChatMessageContent ( threadId , messagesId , content ) { this . setState ( produce ( this . _state , ( draft ) => { const chatMessage = draft . threads [ threadId ] ?. chatMessages [ messagesId ] ; if ( chatMessage ) { if ( ! chatMessage . content ) { chatMessage . content = { } ; } chatMessage . content . message = content ; } } ) ) ; } public deleteLocalMessage ( threadId , localId ) { let localMessageDeleted = false ; this . setState ( produce ( this . _state , ( draft ) => { const chatMessages = draft . threads [ threadId ] ?. chatMessages ; const message = chatMessages ? chatMessages [ localId ] : undefined ; if ( chatMessages && message && message . clientMessageId ) { delete chatMessages [ message . clientMessageId ] ; localMessageDeleted = true ; } } ) ) ; return localMessageDeleted ; } public deleteMessage ( threadId , id ) { this . setState ( produce ( this . _state , ( draft ) => { const chatMessages = draft . threads [ threadId ] ?. chatMessages ; if ( chatMessages ) { delete chatMessages [ id ] ; } } ) ) ; } public setParticipant ( threadId , participant ) { this . setState ( produce ( this . _state , ( draft ) => { const participants = draft . threads [ threadId ] ?. participants ; if ( participants ) { participants [ toFlatCommunicationIdentifier ( participant . id ) ] = participant ; } } ) ) ; } public setParticipants ( threadId , participants ) { this . setState ( produce ( this . _state , ( draft ) => { const participantsMap = draft . threads [ threadId ] ?. participants ; if ( participantsMap ) { for ( const participant of participants ) { participantsMap [ toFlatCommunicationIdentifier ( participant . id ) ] = participant ; } } } ) ) ; } public deleteParticipants ( threadId , participantIds ) { this . setState ( produce ( this . _state , ( draft ) => { const participants = draft . threads [ threadId ] ?. participants ; if ( participants ) { participantIds . forEach ( ( id ) => { delete participants [ toFlatCommunicationIdentifier ( id ) ] ; } ) ; } } ) ) ; } public deleteParticipant ( threadId , participantId ) { this . setState ( produce ( this . _state , ( draft ) => { const participants = draft . threads [ threadId ] ?. participants ; if ( participants ) { delete participants [ toFlatCommunicationIdentifier ( participantId ) ] ; } } ) ) ; } public addReadReceipt ( threadId , readReceipt ) { this . setState ( produce ( this . _state , ( draft ) => { const thread = draft . threads [ threadId ] ; const readReceipts = thread ?. readReceipts ; if ( thread && readReceipts ) { if ( readReceipt . sender !== this . getState ( ) . userId && thread . latestReadTime < readReceipt . readOn ) { thread . latestReadTime = readReceipt . readOn ; } readReceipts . push ( readReceipt ) ; } } ) ) ; } private startTypingIndicatorCleanUp ( ) { if ( ! this . typingIndicatorInterval ) { this . typingIndicatorInterval = window . setInterval ( ( ) => { let isTypingActive = false ; let isStateChanged = false ; const newState = produce ( this . _state , ( draft ) => { for ( const thread of Object . values ( draft . threads ) ) { const filteredTypingIndicators = thread . typingIndicators . filter ( ( typingIndicator ) => { const timeGap = Date . now ( ) - typingIndicator . receivedOn . getTime ( ) ; return timeGap < Constants . TYPING_INDICATOR_MAINTAIN_TIME ; } ) ; if ( thread . typingIndicators . length !== filteredTypingIndicators . length ) { isStateChanged = true ; thread . typingIndicators = filteredTypingIndicators ; } if ( thread . typingIndicators . length > 0 ) { isTypingActive = true ; } } } ) ; if ( isStateChanged ) { this . setState ( newState ) ; } if ( ! isTypingActive && this . typingIndicatorInterval ) { window . clearInterval ( this . typingIndicatorInterval ) ; this . typingIndicatorInterval = undefined ; } } , 1000 ) ; } } public addTypingIndicator ( threadId , typingIndicator ) { this . setState ( produce ( this . _state , ( draft ) => { const thread = draft . threads [ threadId ] ; if ( thread ) { const typingIndicators = thread . typingIndicators ; typingIndicators . push ( typingIndicator ) ; } } ) ) ; this . startTypingIndicatorCleanUp ( ) ; } public setChatMessage ( threadId , message ) { const { id : messageId , clientMessageId } = message ; if ( messageId || clientMessageId ) { this . setState ( produce ( this . _state , ( draft ) => { const threadMessages = draft . threads [ threadId ] ?. chatMessages ; const isLocalIdInMap = threadMessages && clientMessageId && threadMessages [ clientMessageId ] ; const messageKey = ! messageId || isLocalIdInMap ? clientMessageId : messageId ; if ( threadMessages && messageKey ) { threadMessages [ messageKey ] = message ; } const thread = draft . threads [ threadId ] ; if ( thread ) { this . filterTypingIndicatorForUser ( thread , message . sender ) ; } } ) ) ; } } param f param target returns throws public withAsyncErrorTeedToState < Args extends unknown [ ] , R > ( f , target ) { return async ( ... args ) : Promise < R > => { try { return await f ( ... args ) ; } catch ( error ) { const chatError = toChatError ( target , error ) ; this . setLatestError ( target , chatError ) ; throw chatError ; } } ; } param f param target returns throws public withErrorTeedToState < Args extends unknown [ ] , R > ( f , target ) { return ( ... args ) : R => { try { return f ( ... args ) ; } catch ( error ) { const chatError = toChatError ( target , error ) ; this . setLatestError ( target , chatError ) ; throw chatError ; } } ; } private setLatestError ( target , error ) { this . setState ( produce ( this . _state , ( draft ) => { draft . latestErrors [ target ] = error ; } ) ) ; } private filterTypingIndicatorForUser ( thread , userId ? ) { if ( ! userId ) { return ; } const typingIndicators = thread . typingIndicators ; const userIdAsKey = toFlatCommunicationIdentifier ( userId ) ; const filteredTypingIndicators = typingIndicators . filter ( ( typingIndicator ) => toFlatCommunicationIdentifier ( typingIndicator . sender ) !== userIdAsKey ) ; if ( filteredTypingIndicators . length !== typingIndicators . length ) { thread . typingIndicators = filteredTypingIndicators ; } } /**
     * Batch updates to minimize `stateChanged` events across related operations.
     *
     * - A maximum of one `stateChanged` event is emitted, at the end of the operations.
     * - No `stateChanged` event is emitted if the state did not change through the operations.
     * - In case of an exception, state is reset to the prior value and no `stateChanged` event is emitted.
     *
     * All operations finished in this batch should be synchronous.
     * This function is not reentrant -- do not call batch() from within another batch().
     */ public batch ( operations ) { if ( this . _batchMode ) { throw new Error ( 'batch() called from within another batch()' ) ; } this . _batchMode = true ; const priorState = this . _state ; try { operations ( ) ; if ( this . _state !== priorState ) { this . _emitter . emit ( 'stateChanged' , this . _state ) ; } } catch ( e ) { this . _state = priorState ; throw e ; } finally { this . _batchMode = false ; } } public onStateChange ( handler ) { this . _emitter . on ( 'stateChanged' , handler ) ; } public offStateChange ( handler ) { this . _emitter . off ( 'stateChanged' , handler ) ; } } const toChatError = ( target , error ) : ChatError => { if ( error instanceof Error ) { return new ChatError ( target , error ) ; } return new ChatError ( target , new Error ( `${ error }` ) ) ; } ;