UNPKG

@azure/communication-react

Version:

React library for building modern communication user experiences utilizing Azure Communication Services

522 lines • 23.9 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; import { _createStatefulChatClientInner } from "../../../../../chat-stateful-client/src"; import { createDefaultChatHandlers } from "../../../../../chat-component-bindings/src"; import { toFlatCommunicationIdentifier } from "../../../../../acs-ui-common/src"; import { EventEmitter } from 'events'; import { useEffect, useRef, useState } from 'react'; import { _isValidIdentifier } from "../../../../../acs-ui-common/src"; import { TEAMS_LIMITATION_LEARN_MORE, UNSUPPORTED_CHAT_THREAD_TYPE } from '../../common/constants'; import { createProfileStateModifier } from './OnFetchProfileCallback'; /** * Context of Chat, which is a centralized context for all state updates * @private */ export class ChatContext { constructor(clientState, threadId, chatAdapterOptions) { this.emitter = new EventEmitter(); 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 }; this.displayNameModifier = (chatAdapterOptions === null || chatAdapterOptions === void 0 ? void 0 : chatAdapterOptions.onFetchProfile) ? createProfileStateModifier(chatAdapterOptions.onFetchProfile, () => { this.setState(this.getState()); }) : undefined; } onStateChange(handler) { this.emitter.on('stateChanged', handler); } offStateChange(handler) { this.emitter.off('stateChanged', handler); } setState(state) { this.state = state; this.state = this.displayNameModifier ? this.displayNameModifier(state) : state; this.emitter.emit('stateChanged', this.state); } getState() { return this.state; } setError(error) { this.setState(Object.assign(Object.assign({}, this.state), { error })); } updateClientState(clientState) { const thread = clientState.threads[this.threadId]; if (!thread) { throw 'Cannot find threadId, please make sure thread state is still in Stateful ChatClient.'; } let updatedState = { userId: clientState.userId, displayName: clientState.displayName, thread, latestErrors: clientState.latestErrors }; this.setState(updatedState); } } /** * @private */ export class AzureCommunicationChatAdapter { constructor(chatClient, chatThreadClient, chatAdapterOptions) { this.emitter = new EventEmitter(); this.bindAllPublicMethods(); this.chatClient = chatClient; this.chatThreadClient = chatThreadClient; this.context = new ChatContext(chatClient.getState(), chatThreadClient.threadId, chatAdapterOptions); const onStateChange = (clientState) => { // unsubscribe when the instance gets disposed if (!this) { chatClient.offStateChange(onStateChange); return; } this.context.updateClientState(clientState); }; this.handlers = createDefaultChatHandlers(chatClient, chatThreadClient); this.chatClient.onStateChange(onStateChange); this.subscribeAllEvents(); } 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); this.downloadResourceToCache = this.downloadResourceToCache.bind(this); this.removeResourceFromCache = this.removeResourceFromCache.bind(this); } dispose() { this.unsubscribeAllEvents(); this.chatClient.dispose(); } fetchInitialData() { return __awaiter(this, void 0, void 0, function* () { // If get properties fails we dont want to try to get the participants after. yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { var _a, e_1, _b, _c; yield this.chatThreadClient.getProperties(); try { // Fetch all participants who joined before the local user. for (var _d = true, _e = __asyncValues(this.chatThreadClient.listParticipants().byPage({ // Fetch 100 participants per page by default. maxPageSize: 100 })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) { _c = _f.value; _d = false; const _page = _c; ; } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_d && !_a && (_b = _e.return)) yield _b.call(_e); } finally { if (e_1) throw e_1.error; } } })); }); } getState() { return this.context.getState(); } onStateChange(handler) { this.context.onStateChange(handler); } offStateChange(handler) { this.context.offStateChange(handler); } sendMessage(content, options) { return __awaiter(this, void 0, void 0, function* () { yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { return yield this.handlers.onSendMessage(content, options); })); }); } sendReadReceipt(chatMessageId) { return __awaiter(this, void 0, void 0, function* () { yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { yield this.handlers.onMessageSeen(chatMessageId); })); }); } sendTypingIndicator() { return __awaiter(this, void 0, void 0, function* () { yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { yield this.handlers.onTyping(); })); }); } removeParticipant(userId) { return __awaiter(this, void 0, void 0, function* () { yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { yield this.handlers.onRemoveParticipant(userId); })); }); } setTopic(topicName) { return __awaiter(this, void 0, void 0, function* () { yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { yield this.handlers.updateThreadTopicName(topicName); })); }); } loadPreviousChatMessages(messagesToLoad) { return __awaiter(this, void 0, void 0, function* () { return yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { return yield this.handlers.onLoadPreviousChatMessages(messagesToLoad); })); }); } updateMessage(messageId, content, options) { return __awaiter(this, void 0, void 0, function* () { return yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { // metadata is never used in the current stable return yield this.handlers.onUpdateMessage(messageId, content); })); }); } deleteMessage(messageId) { return __awaiter(this, void 0, void 0, function* () { return yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () { return yield this.handlers.onDeleteMessage(messageId); })); }); } downloadResourceToCache(resourceDetails) { return __awaiter(this, void 0, void 0, function* () { this.chatClient.downloadResourceToCache(resourceDetails.threadId, resourceDetails.messageId, resourceDetails.resourceUrl); }); } removeResourceFromCache(resourceDetails) { this.chatClient.removeResourceFromCache(resourceDetails.threadId, resourceDetails.messageId, resourceDetails.resourceUrl); } messageReceivedListener(event) { const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId; if (!isCurrentChatAdapterThread) { return; } const message = convertEventToChatMessage(event); this.emitter.emit('messageReceived', { message }); const currentUserId = toFlatCommunicationIdentifier(this.chatClient.getState().userId); if ((message === null || message === void 0 ? void 0 : message.sender) && toFlatCommunicationIdentifier(message.sender) === currentUserId) { this.emitter.emit('messageSent', { message }); } } messageEditedListener(event) { const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId; if (!isCurrentChatAdapterThread) { return; } const message = convertEventToChatMessage(event); this.emitter.emit('messageEdited', { message }); } messageDeletedListener(event) { const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId; if (!isCurrentChatAdapterThread) { return; } const message = convertEventToChatMessage(event); this.emitter.emit('messageDeleted', { message }); } messageReadListener({ chatMessageId, recipient }) { const message = this.getState().thread.chatMessages[chatMessageId]; if (message) { this.emitter.emit('messageRead', { message, readBy: recipient }); } } participantsAddedListener({ addedBy, participantsAdded }) { this.emitter.emit('participantsAdded', { addedBy, participantsAdded }); } participantsRemovedListener({ removedBy, participantsRemoved }) { this.emitter.emit('participantsRemoved', { removedBy, participantsRemoved }); } chatThreadPropertiesUpdatedListener(event) { this.emitter.emit('topicChanged', { topic: event.properties.topic }); } 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('chatMessageEdited', this.messageEditedListener.bind(this)); this.chatClient.on('chatMessageDeleted', this.messageDeletedListener.bind(this)); this.chatClient.on('readReceiptReceived', this.messageReadListener.bind(this)); this.chatClient.on('participantsRemoved', this.participantsRemovedListener.bind(this)); } 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('chatMessageEdited', this.messageEditedListener.bind(this)); this.chatClient.off('chatMessageDeleted', this.messageDeletedListener.bind(this)); this.chatClient.off('readReceiptReceived', this.messageReadListener.bind(this)); this.chatClient.off('participantsRemoved', this.participantsRemovedListener.bind(this)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event, listener) { this.emitter.on(event, listener); } // eslint-disable-next-line @typescript-eslint/no-explicit-any off(event, listener) { this.emitter.off(event, listener); } asyncTeeErrorToEventEmitter(f) { return __awaiter(this, void 0, void 0, function* () { try { return yield f(); } catch (error) { if (isChatError(error)) { this.emitter.emit('error', error); } throw error; } }); } } const convertEventToChatMessage = (event) => { return { id: event.id, version: event.version, content: isChatMessageDeletedEvent(event) ? undefined : { message: event.message }, type: convertEventType(event.type), sender: event.sender, senderDisplayName: event.senderDisplayName, sequenceId: '', createdOn: new Date(event.createdOn), editedOn: isChatMessageEditedEvent(event) ? event.editedOn : undefined, deletedOn: isChatMessageDeletedEvent(event) ? event.deletedOn : undefined }; }; const isChatMessageEditedEvent = (event) => { return 'editedOn' in event; }; const isChatMessageDeletedEvent = (event) => { return 'deletedOn' in event; }; // only text/html message type will be received from event const convertEventType = (type) => { const lowerCaseType = type.toLowerCase(); if (lowerCaseType === 'richtext/html' || lowerCaseType === 'html') { return 'html'; } else { return 'text'; } }; /** * Create a {@link ChatAdapter} backed by Azure Communication Services. * * This is the default implementation of {@link ChatAdapter} provided by this library. * * @public */ export const createAzureCommunicationChatAdapter = (_a) => __awaiter(void 0, [_a], void 0, function* ({ endpoint: endpointUrl, userId, displayName, credential, threadId, chatAdapterOptions }) { return _createAzureCommunicationChatAdapterInner(endpointUrl, userId, displayName, credential, threadId, 'Chat', chatAdapterOptions); }); /** * This inner function is used to allow injection of TelemetryImplementationHint without changing the public API. * * @internal */ export const _createAzureCommunicationChatAdapterInner = function (endpoint_1, userId_1, displayName_1, credential_1, threadId_1) { return __awaiter(this, arguments, void 0, function* (endpoint, userId, displayName, credential, threadId, telemetryImplementationHint = 'Chat', chatAdapterOptions) { if (!_isValidIdentifier(userId)) { throw new Error('Provided userId is invalid. Please provide valid identifier object.'); } const chatClient = _createStatefulChatClientInner({ userId, displayName, endpoint, credential }, undefined, telemetryImplementationHint); const chatThreadClient = yield chatClient.getChatThreadClient(threadId); yield chatClient.startRealtimeNotifications(); const adapter = yield createAzureCommunicationChatAdapterFromClient(chatClient, chatThreadClient, chatAdapterOptions); return adapter; }); }; /** * This inner function to create ChatAdapterPromise in case when threadID is not avaialble. * ThreadId is a promise to allow for lazy initialization of the adapter. * @internal */ export const _createLazyAzureCommunicationChatAdapterInner = function (endpoint_1, userId_1, displayName_1, credential_1, threadId_1) { return __awaiter(this, arguments, void 0, function* (endpoint, userId, displayName, credential, threadId, telemetryImplementationHint = 'Chat', chatAdapterOptions) { if (!_isValidIdentifier(userId)) { throw new Error('Provided userId is invalid. Please provide valid identifier object.'); } const chatClient = _createStatefulChatClientInner({ userId, displayName, endpoint, credential }, undefined, telemetryImplementationHint); return threadId.then((threadId) => __awaiter(this, void 0, void 0, function* () { if (UNSUPPORTED_CHAT_THREAD_TYPE.some(t => threadId.includes(t))) { console.error(`Invalid Chat ThreadId: ${threadId}. Please note with Teams Channel Meetings, only Calling is supported and Chat is not currently supported. Read more: ${TEAMS_LIMITATION_LEARN_MORE}.`); } const chatThreadClient = yield chatClient.getChatThreadClient(threadId); yield chatClient.startRealtimeNotifications(); const adapter = yield createAzureCommunicationChatAdapterFromClient(chatClient, chatThreadClient, chatAdapterOptions); return adapter; })); }); }; /** * A custom React hook to simplify the creation of {@link ChatAdapter}. * * Similar to {@link createAzureCommunicationChatAdapter}, but takes care of asynchronous * creation of the adapter internally. * * Allows arguments to be undefined so that you can respect the rule-of-hooks and pass in arguments * as they are created. The adapter is only created when all arguments are defined. * * Note that you must memoize the arguments to avoid recreating adapter on each render. * See storybook for typical usage examples. * * @public */ export const useAzureCommunicationChatAdapter = (args, afterCreate, beforeDispose) => { const { credential, displayName, endpoint, threadId, userId } = args; // State update needed to rerender the parent component when a new adapter is created. const [adapter, setAdapter] = useState(undefined); // Ref needed for cleanup to access the old adapter created asynchronously. const adapterRef = useRef(undefined); const creatingAdapterRef = useRef(false); const afterCreateRef = useRef(undefined); const beforeDisposeRef = useRef(undefined); // These refs are updated on *each* render, so that the latest values // are used in the `useEffect` closures below. // Using a Ref ensures that new values for the callbacks do not trigger the // useEffect blocks, and a new adapter creation / distruction is not triggered. afterCreateRef.current = afterCreate; beforeDisposeRef.current = beforeDispose; useEffect(() => { if (!credential || !displayName || !endpoint || !threadId || !userId) { return; } (() => __awaiter(void 0, void 0, void 0, function* () { if (adapterRef.current) { // Dispose the old adapter when a new one is created. // // This clean up function uses `adapterRef` because `adapter` can not be added to the dependency array of // this `useEffect` -- we do not want to trigger a new adapter creation because of the first adapter // creation. if (beforeDisposeRef.current) { yield beforeDisposeRef.current(adapterRef.current); } adapterRef.current.dispose(); adapterRef.current = undefined; } if (creatingAdapterRef.current) { console.warn('Adapter is already being created, please see storybook for more information: https://azure.github.io/communication-ui-library/?path=/story/troubleshooting--page'); return; } creatingAdapterRef.current = true; let newAdapter = yield createAzureCommunicationChatAdapter({ credential, displayName, endpoint, threadId, userId }); if (afterCreateRef.current) { newAdapter = yield afterCreateRef.current(newAdapter); } adapterRef.current = newAdapter; creatingAdapterRef.current = false; setAdapter(newAdapter); }))(); }, // Explicitly list all arguments so that caller doesn't have to memoize the `args` object. [adapterRef, afterCreateRef, beforeDisposeRef, credential, displayName, endpoint, threadId, userId]); // Dispose any existing adapter when the component unmounts. useEffect(() => { return () => { (() => __awaiter(void 0, void 0, void 0, function* () { if (adapterRef.current) { if (beforeDisposeRef.current) { yield beforeDisposeRef.current(adapterRef.current); } adapterRef.current.dispose(); adapterRef.current = undefined; } }))(); }; }, []); return adapter; }; /** * Create a {@link ChatAdapter} using the provided {@link StatefulChatClient}. * * Useful if you want to keep a reference to {@link StatefulChatClient}. * Consider using {@link createAzureCommunicationChatAdapter} for a simpler API. * * @public */ export function createAzureCommunicationChatAdapterFromClient(chatClient, chatThreadClient, chatAdapterOptions) { return __awaiter(this, void 0, void 0, function* () { return new AzureCommunicationChatAdapter(chatClient, chatThreadClient, chatAdapterOptions); }); } const isChatError = (e) => { return 'target' in e && e['target'] !== undefined && 'innerError' in e && e['innerError'] !== undefined; }; //# sourceMappingURL=AzureCommunicationChatAdapter.js.map