@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
522 lines • 23.9 kB
JavaScript
// 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