import { createStatefulChatClient , ChatClientState , ChatError , StatefulChatClient } from '@internal/chat-stateful-client' ; import { ChatHandlers , createDefaultChatHandlers } from '@internal/chat-component-bindings' ; import { ChatMessage , ChatMessageType , ChatThreadClient } from '@azure/communication-chat' ; import { CommunicationTokenCredential , CommunicationUserIdentifier } from '@azure/communication-common' ; import type { ChatMessageReceivedEvent , ChatThreadPropertiesUpdatedEvent , ParticipantsAddedEvent , ParticipantsRemovedEvent , ReadReceiptReceivedEvent } from '@azure/communication-signaling' ; import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common' ; import EventEmitter from 'events' ; import { ChatAdapter , ChatAdapterState , MessageReadListener , MessageReceivedListener , ParticipantsAddedListener , ParticipantsRemovedListener , TopicChangedListener } from './ChatAdapter' ; import { AdapterError } from '../../common/adapters' ; /** Context of Chat, which is a centralized context for all state updates */ class ChatContext { private emitter : EventEmitter = new EventEmitter ( ) ; private state : ChatAdapterState ; private threadId : string ; constructor ( clientState , threadId ) { const thread = clientState . threads [ threadId ] ; this . threadId = threadId ; if ( ! thread ) { throw 'Cannot find threadId, please initialize thread before use!' ; } this . state = { userId : clientState . userId , displayName : clientState . displayName , thread , latestErrors : clientState . latestErrors } ; } public onStateChange ( handler ) { this . emitter . on ( 'stateChanged' , handler ) ; } public offStateChange ( handler ) { this . emitter . off ( 'stateChanged' , handler ) ; } public setState ( state ) { this . state = state ; this . emitter . emit ( 'stateChanged' , this . state ) ; } public getState ( ) { return this . state ; } public setError ( error ) { this . setState ( { ... this . state , error } ) ; } public updateClientState ( clientState ) { const thread = clientState . threads [ this . threadId ] ; if ( ! thread ) { throw 'Cannot find threadId, please make sure thread state is still in Stateful ChatClient.' ; } this . setState ( { userId : clientState . userId , displayName : clientState . displayName , thread , latestErrors : clientState . latestErrors } ) ; } } private export class AzureCommunicationChatAdapter implements ChatAdapter { private chatClient : StatefulChatClient ; private chatThreadClient : ChatThreadClient ; private context : ChatContext ; private handlers : ChatHandlers ; private emitter : EventEmitter = new EventEmitter ( ) ; constructor ( chatClient , chatThreadClient ) { this . bindAllPublicMethods ( ) ; this . chatClient = chatClient ; this . chatThreadClient = chatThreadClient ; this . context = new ChatContext ( chatClient . getState ( ) , chatThreadClient . threadId ) ; const onStateChange = ( clientState ) : void => { if ( ! this ) { chatClient . offStateChange ( onStateChange ) ; return ; } this . context . updateClientState ( clientState ) ; } ; this . handlers = createDefaultChatHandlers ( chatClient , chatThreadClient ) ; this . chatClient . onStateChange ( onStateChange ) ; this . subscribeAllEvents ( ) ; } private bindAllPublicMethods ( ) { this . onStateChange = this . onStateChange . bind ( this ) ; this . offStateChange = this . offStateChange . bind ( this ) ; this . getState = this . getState . bind ( this ) ; this . dispose = this . dispose . bind ( this ) ; this . fetchInitialData = this . fetchInitialData . bind ( this ) ; this . sendMessage = this . sendMessage . bind ( this ) ; this . sendReadReceipt = this . sendReadReceipt . bind ( this ) ; this . sendTypingIndicator = this . sendTypingIndicator . bind ( this ) ; this . updateMessage = this . updateMessage . bind ( this ) ; this . deleteMessage = this . deleteMessage . bind ( this ) ; this . removeParticipant = this . removeParticipant . bind ( this ) ; this . setTopic = this . setTopic . bind ( this ) ; this . loadPreviousChatMessages = this . loadPreviousChatMessages . bind ( this ) ; this . on = this . on . bind ( this ) ; this . off = this . off . bind ( this ) ; } dispose ( ) { this . unsubscribeAllEvents ( ) ; } async fetchInitialData ( ) { try { await this . chatThreadClient . getProperties ( ) ; } catch ( e ) { console . log ( e ) ; } try { for await ( const _page of this . chatThreadClient . listParticipants ( ) . byPage ( { maxPageSize : 100 } ) ) ; } catch ( e ) { console . log ( e ) ; } } getState ( ) { return this . context . getState ( ) ; } onStateChange ( handler ) { this . context . onStateChange ( handler ) ; } offStateChange ( handler ) { this . context . offStateChange ( handler ) ; } async sendMessage ( content ) { await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . handlers . onSendMessage ( content ) ; } ) ; } async sendReadReceipt ( chatMessageId ) { await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . handlers . onMessageSeen ( chatMessageId ) ; } ) ; } async sendTypingIndicator ( ) { await this . handlers . onTyping ( ) ; } async removeParticipant ( userId ) { await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . handlers . onRemoveParticipant ( userId ) ; } ) ; } async setTopic ( topicName ) { await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . handlers . updateThreadTopicName ( topicName ) ; } ) ; } async loadPreviousChatMessages ( messagesToLoad ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { return await this . handlers . onLoadPreviousChatMessages ( messagesToLoad ) ; } ) ; } async updateMessage ( messageId , content ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { return await this . handlers . onUpdateMessage ( messageId , content ) ; } ) ; } async deleteMessage ( messageId ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { return await this . handlers . onDeleteMessage ( messageId ) ; } ) ; } private messageReceivedListener ( event ) { const message = convertEventToChatMessage ( event ) ; this . emitter . emit ( 'messageReceived' , { message } ) ; const currentUserId = toFlatCommunicationIdentifier ( this . chatClient . getState ( ) . userId ) ; if ( message ?. sender && toFlatCommunicationIdentifier ( message . sender ) === currentUserId ) { this . emitter . emit ( 'messageSent' , { message } ) ; } } private messageReadListener ( { chatMessageId , recipient } ) { const message = this . getState ( ) . thread . chatMessages [ chatMessageId ] ; if ( message ) { this . emitter . emit ( 'messageRead' , { message , readBy : recipient } ) ; } } private participantsAddedListener ( { addedBy , participantsAdded } ) { this . emitter . emit ( 'participantsAdded' , { addedBy , participantsAdded } ) ; } private participantsRemovedListener ( { removedBy , participantsRemoved } ) { this . emitter . emit ( 'participantsRemoved' , { removedBy , participantsRemoved } ) ; } private chatThreadPropertiesUpdatedListener ( event ) { this . emitter . emit ( 'topicChanged' , { topic : event . properties . topic } ) ; } private subscribeAllEvents ( ) { this . chatClient . on ( 'chatThreadPropertiesUpdated' , this . chatThreadPropertiesUpdatedListener . bind ( this ) ) ; this . chatClient . on ( 'participantsAdded' , this . participantsAddedListener . bind ( this ) ) ; this . chatClient . on ( 'participantsRemoved' , this . participantsRemovedListener . bind ( this ) ) ; this . chatClient . on ( 'chatMessageReceived' , this . messageReceivedListener . bind ( this ) ) ; this . chatClient . on ( 'readReceiptReceived' , this . messageReadListener . bind ( this ) ) ; this . chatClient . on ( 'participantsRemoved' , this . participantsRemovedListener . bind ( this ) ) ; } private unsubscribeAllEvents ( ) { this . chatClient . off ( 'chatThreadPropertiesUpdated' , this . chatThreadPropertiesUpdatedListener . bind ( this ) ) ; this . chatClient . off ( 'participantsAdded' , this . participantsAddedListener . bind ( this ) ) ; this . chatClient . off ( 'participantsRemoved' , this . participantsRemovedListener . bind ( this ) ) ; this . chatClient . off ( 'chatMessageReceived' , this . messageReceivedListener . bind ( this ) ) ; this . chatClient . off ( 'readReceiptReceived' , this . messageReadListener . bind ( this ) ) ; this . chatClient . off ( 'participantsRemoved' , this . participantsRemovedListener . bind ( this ) ) ; } on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) { this . emitter . on ( event , listener ) ; } off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) { this . emitter . off ( event , listener ) ; } private async asyncTeeErrorToEventEmitter < T > ( f ) { try { return await f ( ) ; } catch ( error ) { if ( isChatError ( error ) ) { this . emitter . emit ( 'error' , error as AdapterError ) ; } throw error ; } } } const convertEventToChatMessage = ( event ) : ChatMessage => { return { id : event . id , version : event . version , content : { message : event . message } , type : convertEventType ( event . type ) , sender : event . sender , senderDisplayName : event . senderDisplayName , sequenceId : '' , createdOn : new Date ( event . createdOn ) } ; } ; const convertEventType = ( type ) : ChatMessageType => { const lowerCaseType = type . toLowerCase ( ) ; if ( lowerCaseType === 'richtext/html' || lowerCaseType === 'html' ) { return 'html' ; } else { return 'text' ; } } ; /**
 * Arguments for creating the Azure Communication Services implementation of  ChatAdapter .
 *
 *  public export type AzureCommunicationChatAdapterArgs = { endpoint : string ; userId : CommunicationUserIdentifier ; displayName : string ; credential : CommunicationTokenCredential ; threadId : string ; } ; /**
 * Create a  ChatAdapter backed by Azure Communication Services.
 *
 * This is the default implementation of  ChatAdapter provided by this library.
 *
 *  public export const createAzureCommunicationChatAdapter = async ( { endpoint : endpointUrl , userId , displayName , credential , threadId } ) : Promise < ChatAdapter > => { const chatClient = createStatefulChatClient ( { userId , displayName , endpoint : endpointUrl , credential : credential } ) ; const chatThreadClient = await chatClient . getChatThreadClient ( threadId ) ; await chatClient . startRealtimeNotifications ( ) ; const adapter = await createAzureCommunicationChatAdapterFromClient ( chatClient , chatThreadClient ) ; await adapter . fetchInitialData ( ) ; return adapter ; } ; /**
 * Create a  ChatAdapter using the provided  StatefulChatClient .
 *
 * Useful if you want to keep a reference to  StatefulChatClient .
 * Consider using  createAzureCommunicationChatAdapter for a simpler API.
 *
 *  public export const createAzureCommunicationChatAdapterFromClient = async ( chatClient , chatThreadClient ) : Promise < ChatAdapter > => { return new AzureCommunicationChatAdapter ( chatClient , chatThreadClient ) ; } ; const isChatError = ( e ) : e is ChatError => { return e [ 'target' ] !== undefined && e [ 'innerError' ] !== undefined ; } ;