packages/react-composites/src/composites/ChatComposite/adapter/OnFetchProfileCallback.ts (209 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/* @conditional-compile-remove(on-fetch-profile) */
import { ChatParticipant } from '@azure/communication-chat';
/* @conditional-compile-remove(on-fetch-profile) */
import { AdapterStateModifier } from './AzureCommunicationChatAdapter';
/* @conditional-compile-remove(on-fetch-profile) */
import { ChatAdapterState } from './ChatAdapter';
/* @conditional-compile-remove(on-fetch-profile) */
import { ChatMessageWithStatus } from '@internal/chat-stateful-client';
/* @conditional-compile-remove(on-fetch-profile) */
/**
* Callback function used to provide custom data to build profile for a user or bot.
*
* @public
*/
export type OnFetchChatProfileCallback = (
userId: string,
defaultProfile?: ChatProfile
) => Promise<ChatProfile | undefined>;
/* @conditional-compile-remove(on-fetch-profile) */
/**
* The profile of a user or bot.
*
* @public
*/
export type ChatProfile = {
/**
* Primary text to display, usually the name of the person.
*/
displayName?: string;
};
/* @conditional-compile-remove(on-fetch-profile) */
/**
* @private
*/
export const createProfileStateModifier = (
onFetchProfile: OnFetchChatProfileCallback,
notifyUpdate: () => void
): AdapterStateModifier => {
const cachedDisplayName: {
[id: string]: string;
} = {};
return (state: ChatAdapterState) => {
const originalParticipants = state.thread?.participants;
(async () => {
let shouldNotifyUpdates = false;
if (!originalParticipants) {
return;
}
for (const [key, participant] of Object.entries(originalParticipants)) {
if (cachedDisplayName[key]) {
continue;
}
const profile = await onFetchProfile(key, { displayName: participant.displayName });
if (profile?.displayName && participant.displayName !== profile?.displayName) {
cachedDisplayName[key] = profile?.displayName;
shouldNotifyUpdates = true;
}
}
// notify update only when there is a change, which most likely will trigger modifier and setState again
if (shouldNotifyUpdates) {
notifyUpdate();
}
})();
const participantsModifier = createParticipantModifier(
(id: string, participant: ChatParticipant): ChatParticipant | undefined => {
if (cachedDisplayName[id]) {
return { ...participant, displayName: cachedDisplayName[id] };
}
return undefined;
}
);
const modifiedParticipantState = participantsModifier(state);
const chatMessagesModifier = createChatMessageModifier(
(id: string, chatMessage: ChatMessageWithStatus): ChatMessageWithStatus | undefined => {
const originalChatMessage = { ...chatMessage };
if (originalChatMessage.content?.participants) {
const newParticipants = originalChatMessage.content.participants.map((participant: ChatParticipant) => {
if (participant.id) {
if ('communicationUserId' in participant.id && cachedDisplayName[participant.id.communicationUserId]) {
return { ...participant, displayName: cachedDisplayName[participant.id.communicationUserId] };
} else if (
'microsoftTeamsUserId' in participant.id &&
'rawId' in participant.id &&
participant.id.rawId &&
cachedDisplayName[participant.id.rawId]
) {
return { ...participant, displayName: cachedDisplayName[participant.id.rawId] };
} else if (
'teamsAppId' in participant.id &&
'rawId' in participant.id &&
participant.id.rawId &&
cachedDisplayName[participant.id.rawId]
) {
return { ...participant, displayName: cachedDisplayName[participant.id.rawId] };
} else if (
'phoneNumber' in participant.id &&
'rawId' in participant.id &&
participant.id.rawId &&
cachedDisplayName[participant.id.rawId]
) {
return { ...participant, displayName: cachedDisplayName[participant.id.rawId] };
} else if ('id' in participant.id && cachedDisplayName[participant.id.id]) {
return { ...participant, displayName: cachedDisplayName[participant.id.id] };
} else {
return participant;
}
}
return participant;
});
originalChatMessage.content = {
...originalChatMessage.content,
participants: newParticipants
};
}
if (originalChatMessage.sender && originalChatMessage.senderDisplayName) {
if (
originalChatMessage.sender.kind === 'communicationUser' &&
originalChatMessage.sender.communicationUserId &&
cachedDisplayName[originalChatMessage.sender.communicationUserId]
) {
originalChatMessage.senderDisplayName = cachedDisplayName[originalChatMessage.sender.communicationUserId];
} else if (
originalChatMessage.sender.kind === 'microsoftTeamsUser' &&
originalChatMessage.sender.rawId &&
cachedDisplayName[originalChatMessage.sender.rawId]
) {
originalChatMessage.senderDisplayName = cachedDisplayName[originalChatMessage.sender.rawId];
} else if (
originalChatMessage.sender.kind === 'phoneNumber' &&
originalChatMessage.sender.phoneNumber &&
cachedDisplayName[originalChatMessage.sender.phoneNumber]
) {
originalChatMessage.senderDisplayName = cachedDisplayName[originalChatMessage.sender.phoneNumber];
} else if (
originalChatMessage.sender.kind === 'unknown' &&
originalChatMessage.sender.id &&
cachedDisplayName[originalChatMessage.sender.id]
) {
originalChatMessage.senderDisplayName = cachedDisplayName[originalChatMessage.sender.id];
} else if (
originalChatMessage.sender.kind === 'microsoftTeamsApp' &&
originalChatMessage.sender.rawId &&
cachedDisplayName[originalChatMessage.sender.rawId]
) {
originalChatMessage.senderDisplayName = cachedDisplayName[originalChatMessage.sender.rawId];
}
}
return { ...originalChatMessage };
}
);
return chatMessagesModifier(modifiedParticipantState);
};
};
/* @conditional-compile-remove(on-fetch-profile) */
/**
* @private
* This is the util function to create a participant modifier for remote participantList
* It memoize previous original participant items and only update the changed participant
* It takes in one modifier function to generate one single participant object, it returns undefined if the object keeps unmodified
*/
export const createParticipantModifier = (
createModifiedParticipant: (id: string, participant: ChatParticipant) => ChatParticipant | undefined
): AdapterStateModifier => {
let previousParticipantState:
| {
[keys: string]: ChatParticipant;
}
| undefined = undefined;
let modifiedParticipants: {
[keys: string]: ChatParticipant;
} = {};
const memoizedParticipants: {
[id: string]: { originalRef: ChatParticipant; newParticipant: ChatParticipant };
} = {};
return (state: ChatAdapterState) => {
// if root state is the same, we don't need to update the participants
if (state.thread?.participants !== previousParticipantState) {
modifiedParticipants = {};
const originalParticipants = Object.entries(state.thread?.participants || {});
for (const [key, originalParticipant] of originalParticipants) {
const modifiedParticipant = createModifiedParticipant(key, originalParticipant);
if (modifiedParticipant === undefined) {
modifiedParticipants[key] = originalParticipant;
continue;
}
// Generate the new item if original cached item has been changed
if (memoizedParticipants[key]?.originalRef !== originalParticipant) {
memoizedParticipants[key] = {
newParticipant: modifiedParticipant,
originalRef: originalParticipant
};
}
// the modified participant is always coming from the memoized cache, whether is was refreshed
// from the previous closure or not
const memoizedParticipant = memoizedParticipants[key];
if (!memoizedParticipant) {
throw new Error('Participant modifier encountered an unhandled exception.');
}
modifiedParticipants[key] = memoizedParticipant.newParticipant;
}
previousParticipantState = state.thread?.participants;
}
return { ...state, thread: { ...state.thread, participants: modifiedParticipants } };
};
};
/* @conditional-compile-remove(on-fetch-profile) */
/**
* @private
* This is the util function to create a chat message modifier for remote participantList
* It memoize previous original messages and only update the changed sender display name
* It takes in one modifier function to generate one single participant object, it returns undefined if the object keeps unmodified
*/
export const createChatMessageModifier = (
createModifiedChatMessage: (id: string, chatMessage: ChatMessageWithStatus) => ChatMessageWithStatus | undefined
): AdapterStateModifier => {
let previousChatMessages: {
[key: string]: ChatMessageWithStatus;
};
let modifiedChatMessages: {
[keys: string]: ChatMessageWithStatus;
};
const memoizedChatMessages: {
[id: string]: { originalRef: ChatMessageWithStatus; newChatMessage: ChatMessageWithStatus };
} = {};
return (state: ChatAdapterState) => {
if (state.thread?.chatMessages !== previousChatMessages) {
modifiedChatMessages = {};
const originalChatMessages = Object.entries(state.thread?.chatMessages || {});
for (const [key, originalChatMessage] of originalChatMessages) {
const modifiedChatMessage = createModifiedChatMessage(key, originalChatMessage);
if (modifiedChatMessage === undefined) {
modifiedChatMessages[key] = originalChatMessage;
continue;
}
// Generate the new item if original cached item has been changed
if (memoizedChatMessages[key]?.originalRef !== originalChatMessage) {
memoizedChatMessages[key] = {
newChatMessage: modifiedChatMessage,
originalRef: originalChatMessage
};
}
// the modified chat message is always coming from the memoized cache, whether is was refreshed
// from the previous closure or not
const memoizedChatMessage = memoizedChatMessages[key];
if (!memoizedChatMessage) {
throw new Error('Participant modifier encountered an unhandled exception.');
}
modifiedChatMessages[key] = memoizedChatMessage.newChatMessage;
}
previousChatMessages = state.thread?.chatMessages;
}
return { ...state, thread: { ...state.thread, chatMessages: modifiedChatMessages } };
};
};