Clean up states, hooks and mappers in chat (#176)
* clean up states, hooks and mappers in chat * Change files * add prefetch participants list * fix dependency circle * update imports to the chat thread helper in samples Co-authored-by: Eason Yang <easony@microsoft.com>
This commit is contained in:
Родитель
3a517bbf16
Коммит
edb4828cac
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "prerelease",
|
||||
"comment": "clean up states, hooks and mappers in chat",
|
||||
"packageName": "@azure/communication-ui",
|
||||
"email": "easony@microsoft.com",
|
||||
"dependentChangeType": "patch"
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { chatThreadSelector, sendBoxSelector, typingIndicatorSelector } from '@azure/acs-chat-selector';
|
||||
import { mergeStyles, Stack } from '@fluentui/react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { ErrorBar, MessageThread, SendBox, TypingIndicator } from '../../components';
|
||||
import { useChatThreadClient } from '../../providers';
|
||||
import { useHandlers } from './hooks/useHandlers';
|
||||
import { useSelector } from './hooks/useSelector';
|
||||
import { chatContainer, chatWrapper } from './styles/GroupChat.styles';
|
||||
|
||||
export type ChatScreenProps = {
|
||||
threadId: string;
|
||||
sendBoxMaxLength?: number;
|
||||
onRenderAvatar?: (userId: string) => JSX.Element;
|
||||
};
|
||||
|
||||
export const ChatScreen = (props: ChatScreenProps): JSX.Element => {
|
||||
const { threadId, onRenderAvatar, sendBoxMaxLength } = props;
|
||||
|
||||
const pixelToRemConvertRatio = 16;
|
||||
const defaultNumberOfChatMessagesToReload = 5;
|
||||
const sendBoxParentStyle = mergeStyles({
|
||||
maxWidth: sendBoxMaxLength ? `${sendBoxMaxLength / pixelToRemConvertRatio}rem` : 'unset',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
const chatThreadClient = useChatThreadClient();
|
||||
|
||||
// This code gets all participants who joined the chat earlier than the current user.
|
||||
// We need to do this to make the state in declaritive up to date.
|
||||
useEffect(() => {
|
||||
const fetchAllParticipants = async (): Promise<void> => {
|
||||
if (chatThreadClient !== undefined) {
|
||||
try {
|
||||
for await (const _page of chatThreadClient.listParticipants().byPage({
|
||||
// Fetch 100 participants per page by default.
|
||||
maxPageSize: 100
|
||||
}));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchAllParticipants();
|
||||
}, [chatThreadClient]);
|
||||
|
||||
const selectorConfig = useMemo(() => {
|
||||
return { threadId };
|
||||
}, [threadId]);
|
||||
const sendBoxProps = useSelector(sendBoxSelector, selectorConfig);
|
||||
const sendBoxHandlers = useHandlers(SendBox);
|
||||
const typingIndicatorProps = useSelector(typingIndicatorSelector, selectorConfig);
|
||||
const chatThreadProps = useSelector(chatThreadSelector, selectorConfig);
|
||||
const chatThreadHandlers = useHandlers(MessageThread);
|
||||
|
||||
return (
|
||||
<Stack className={chatContainer} grow>
|
||||
<Stack className={chatWrapper} grow>
|
||||
<MessageThread
|
||||
{...chatThreadProps}
|
||||
{...chatThreadHandlers}
|
||||
onRenderAvatar={onRenderAvatar}
|
||||
numberOfChatMessagesToReload={defaultNumberOfChatMessagesToReload}
|
||||
/>
|
||||
<Stack.Item align="center" className={sendBoxParentStyle}>
|
||||
<div style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
|
||||
<TypingIndicator {...typingIndicatorProps} />
|
||||
</div>
|
||||
<ErrorBar />
|
||||
<SendBox {...sendBoxProps} {...sendBoxHandlers} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
|
@ -1,24 +1,11 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
import { Stack } from '@fluentui/react';
|
||||
import { mergeStyles } from '@fluentui/react';
|
||||
|
||||
import { ChatProvider } from '../../providers';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
SendBox,
|
||||
TypingIndicator,
|
||||
ErrorBar as ErrorBarComponent,
|
||||
MessageThread,
|
||||
MessageThreadProps
|
||||
} from '../../components';
|
||||
import { chatContainer, chatWrapper } from './styles/GroupChat.styles';
|
||||
import React from 'react';
|
||||
import { AbortSignalLike } from '@azure/core-http';
|
||||
import { ErrorHandlingProps, ErrorProvider } from '../../providers/ErrorProvider';
|
||||
import { ErrorProvider } from '../../providers/ErrorProvider';
|
||||
import { CommunicationUiErrorInfo } from '../../types/CommunicationUiError';
|
||||
import { connectFuncsToContext, MapToChatMessageProps, MapToErrorBarProps } from '../../consumers';
|
||||
import { WithErrorHandling } from '../../utils';
|
||||
import { typingIndicatorSelector, sendBoxSelector } from '@azure/acs-chat-selector';
|
||||
import { useSelector } from './hooks/useSelector';
|
||||
import { useHandlers } from './hooks/useHandlers';
|
||||
import { ChatScreen } from './ChatScreen';
|
||||
|
||||
export type GroupChatProps = {
|
||||
displayName: string;
|
||||
|
@ -39,31 +26,6 @@ type GroupChatOptions = {
|
|||
|
||||
export default (props: GroupChatProps): JSX.Element => {
|
||||
const { displayName, threadId, token, endpointUrl, options, onErrorCallback, onRenderAvatar } = props;
|
||||
const sendBoxParentStyle = mergeStyles({
|
||||
maxWidth: options?.sendBoxMaxLength ? `${options?.sendBoxMaxLength / 16}rem` : 'unset',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
const ChatThread = useMemo(() => {
|
||||
return connectFuncsToContext(
|
||||
(props: MessageThreadProps & ErrorHandlingProps) => WithErrorHandling(MessageThread, props),
|
||||
() => {
|
||||
return {
|
||||
...MapToChatMessageProps(),
|
||||
onRenderAvatar: onRenderAvatar
|
||||
};
|
||||
}
|
||||
);
|
||||
}, [onRenderAvatar]);
|
||||
const ErrorBar = useMemo(() => {
|
||||
return connectFuncsToContext(ErrorBarComponent, MapToErrorBarProps);
|
||||
}, []);
|
||||
const selectorConfig = useMemo(() => {
|
||||
return { threadId };
|
||||
}, [threadId]);
|
||||
const sendBoxProps = useSelector(sendBoxSelector, selectorConfig);
|
||||
const sendBoxHandlers = useHandlers(SendBox);
|
||||
const typingIndicatorProps = useSelector(typingIndicatorSelector, selectorConfig);
|
||||
|
||||
return (
|
||||
<ErrorProvider onErrorCallback={onErrorCallback}>
|
||||
|
@ -75,18 +37,7 @@ export default (props: GroupChatProps): JSX.Element => {
|
|||
refreshTokenCallback={props.refreshTokenCallback}
|
||||
>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
|
||||
<Stack className={chatContainer} grow>
|
||||
<Stack className={chatWrapper} grow>
|
||||
<ChatThread />
|
||||
<Stack.Item align="center" className={sendBoxParentStyle}>
|
||||
<div style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
|
||||
<TypingIndicator {...typingIndicatorProps} />
|
||||
</div>
|
||||
<ErrorBar />
|
||||
<SendBox {...sendBoxProps} {...sendBoxHandlers} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ChatScreen threadId={threadId} sendBoxMaxLength={options?.sendBoxMaxLength} onRenderAvatar={onRenderAvatar} />
|
||||
</ChatProvider>
|
||||
</ErrorProvider>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { DeclarativeChatClient } from '@azure/acs-chat-declarative';
|
||||
import { createDefaultHandlersForComponent } from '@azure/acs-chat-selector';
|
||||
|
||||
import { useChatClient } from '../../../providers/ChatProviderHelper';
|
||||
import { useChatThreadClient } from '../../../providers/ChatThreadProvider';
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatClientState, DeclarativeChatClient } from '@azure/acs-chat-declarative';
|
||||
import { useChatClient } from '../../../providers/ChatProviderHelper';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useChatClient } from '../../../providers/ChatProviderHelper';
|
||||
|
||||
// This function highly depends on chatClient.onChange event
|
||||
// It will be moved into selector folder when the ChatClientProvide when refactor finished
|
||||
|
|
|
@ -1,259 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatParticipant } from '@azure/communication-chat';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
useSubscribeReadReceipt,
|
||||
useSubscribeMessage,
|
||||
useIsMessageSeen,
|
||||
useSendReadReceipt,
|
||||
useFetchMessages,
|
||||
ChatMessageWithClientMessageId
|
||||
} from '../hooks';
|
||||
import { useChatMessages, useFailedMessageIds, useThreadMembers, useUserId } from '../providers';
|
||||
import { compareMessages } from '../utils';
|
||||
import { PARTICIPANTS_THRESHOLD, PAGE_SIZE } from '../constants';
|
||||
import { ChatMessagePayload, ChatMessage, MessageStatus } from '../types';
|
||||
|
||||
export const updateMessagesWithAttached = (
|
||||
chatMessagesWithStatus: ChatMessagePayload[],
|
||||
userId: string,
|
||||
failedMessageIds: string[],
|
||||
isLargeGroup: boolean,
|
||||
isMessageSeen: (userId: string, message: ChatMessagePayload) => boolean
|
||||
): ChatMessage[] => {
|
||||
/**
|
||||
* A block of messages: continuous messages that belong to the same sender and not intercepted by other senders.
|
||||
*
|
||||
* This is the index of the last message in the previous block of messages that are mine.
|
||||
* This message's statusToRender will be reset when there's a new block of messages that are mine. (Because
|
||||
* in this case, we only want to show the read statusToRender of last message of the new messages block)
|
||||
*/
|
||||
let IndexOfMyLastMassage: number | undefined = undefined;
|
||||
const newChatMessages: ChatMessage[] = [];
|
||||
|
||||
chatMessagesWithStatus.sort(compareMessages);
|
||||
chatMessagesWithStatus.map((message: any, index: number, messagesList: any) => {
|
||||
const mine = message.senderId === userId;
|
||||
let attached: string | boolean = false;
|
||||
if (index === 0) {
|
||||
if (index !== messagesList.length - 1) {
|
||||
//the next message has the same sender
|
||||
if (messagesList[index].senderId === messagesList[index + 1].senderId) {
|
||||
attached = 'top';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (messagesList[index].senderId === messagesList[index - 1].senderId) {
|
||||
//the previous message has the same sender
|
||||
if (index !== messagesList.length - 1) {
|
||||
if (messagesList[index].senderId === messagesList[index + 1].senderId) {
|
||||
//the next message has the same sender
|
||||
attached = true;
|
||||
} else {
|
||||
//the next message has a different sender
|
||||
attached = 'bottom';
|
||||
}
|
||||
} else {
|
||||
// this is the last message of the whole messages list
|
||||
attached = 'bottom';
|
||||
}
|
||||
} else {
|
||||
//the previous message has a different sender
|
||||
if (index !== messagesList.length - 1) {
|
||||
if (messagesList[index].senderId === messagesList[index + 1].senderId) {
|
||||
//the next message has the same sender
|
||||
attached = 'top';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only set the statusToRender of a message if the message is mine
|
||||
let statusToRender: MessageStatus | undefined = undefined;
|
||||
if (mine) {
|
||||
statusToRender = getMessageStatus(message, failedMessageIds, isLargeGroup, userId, isMessageSeen);
|
||||
|
||||
// Clean the statusToRender of the previous message in the same message block of mine.
|
||||
if (newChatMessages.length > 0) {
|
||||
const prevMsg = newChatMessages[newChatMessages.length - 1];
|
||||
if (prevMsg.payload.status === statusToRender || prevMsg.payload.status === 'failed') {
|
||||
prevMsg.payload.status = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a previous block of messages that are mine, clean the read statusToRender on the last message
|
||||
if (IndexOfMyLastMassage) {
|
||||
newChatMessages[IndexOfMyLastMassage].payload.status = undefined;
|
||||
IndexOfMyLastMassage = undefined;
|
||||
}
|
||||
|
||||
// Update IndexOfMyLastMassage to be the index of last message in this block.
|
||||
if (messagesList[index + 1]?.senderId !== userId) {
|
||||
IndexOfMyLastMassage = index;
|
||||
}
|
||||
}
|
||||
|
||||
const messageWithAttached = { ...message, attached, mine, statusToRender };
|
||||
// Remove the clientMessageId field as it's only needed to getMessageStatus, not needed by MessageThread component
|
||||
// When we migrate to declarative, ideally we should remove the clientMessageId from the ChatMessagePayload type.
|
||||
delete messageWithAttached.clientMessageId;
|
||||
newChatMessages.push({
|
||||
type: 'chat',
|
||||
payload: messageWithAttached
|
||||
});
|
||||
});
|
||||
return newChatMessages;
|
||||
};
|
||||
|
||||
export const getLatestIncomingMessageId = (chatMessages: ChatMessage[], userId: string): string | undefined => {
|
||||
const lastSeenChatMessage = chatMessages
|
||||
.filter((message) => message.payload.createdOn && message.payload.senderId !== userId)
|
||||
.map((message) => ({ createdOn: message.payload.createdOn, id: message.payload.messageId }))
|
||||
.reduce(
|
||||
(message1, message2) => {
|
||||
if (!message1.createdOn || !message2.createdOn) {
|
||||
return message1.createdOn ? message1 : message2;
|
||||
} else {
|
||||
return compareMessages(message1, message2) > 0 ? message1 : message2;
|
||||
}
|
||||
},
|
||||
{ createdOn: undefined, id: undefined }
|
||||
);
|
||||
return lastSeenChatMessage.id;
|
||||
};
|
||||
|
||||
export const getMessageStatus = (
|
||||
message: ChatMessagePayload,
|
||||
failedMessageIds: string[],
|
||||
isLargeParticipantsGroup: boolean,
|
||||
userId: string,
|
||||
isMessageSeen?: ((userId: string, message: ChatMessagePayload) => boolean) | undefined
|
||||
): MessageStatus => {
|
||||
// message is pending send or is failed to be sent
|
||||
if (message.createdOn === undefined || message.messageId === '') {
|
||||
const messageFailed: boolean =
|
||||
failedMessageIds.find((failedMessageId: string) => failedMessageId === message.clientMessageId) !== undefined;
|
||||
return messageFailed ? 'failed' : 'sending';
|
||||
} else {
|
||||
if (message.messageId === undefined) return 'delivered';
|
||||
// show read receipt if it's not a large participant group
|
||||
if (!isLargeParticipantsGroup) {
|
||||
return isMessageSeen && isMessageSeen(userId, message) ? 'seen' : 'delivered';
|
||||
} else {
|
||||
return 'delivered';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isLargeParticipantsGroup = (threadMembers: ChatParticipant[]): boolean => {
|
||||
return threadMembers.length >= PARTICIPANTS_THRESHOLD;
|
||||
};
|
||||
|
||||
/**
|
||||
* In order to display chat message on screen with all necessary components like message ordering, read receipt, failed
|
||||
* messages, etc., we need information from many different places in Chat SDK. But to provide a nice clean interface for
|
||||
* developers, we hide all of that by combining all different sources of info before passing it down as a prop to
|
||||
* MessageThread. This way we keep the Chat SDK parts internal and if developer wants to use this component with their own
|
||||
* data source, they only need to provide one stream of formatted ChatMessagePayload[] for MessageThread to be able to render
|
||||
* everything properly.
|
||||
*
|
||||
* @param chatMessages
|
||||
* @param failedMessageIds
|
||||
* @param isLargeGroup
|
||||
* @param userId
|
||||
* @param isMessageSeen
|
||||
*/
|
||||
const convertSdkChatMessagesToWebUiChatMessages = (
|
||||
chatMessages: ChatMessageWithClientMessageId[],
|
||||
failedMessageIds: string[],
|
||||
isLargeGroup: boolean,
|
||||
userId: string,
|
||||
isMessageSeen: (userId: string, message: ChatMessagePayload) => boolean
|
||||
): ChatMessage[] => {
|
||||
const convertedChatMessages =
|
||||
chatMessages?.map<ChatMessagePayload>((chatMessage: ChatMessageWithClientMessageId) => {
|
||||
return {
|
||||
messageId: chatMessage.id,
|
||||
content: chatMessage.content?.message ?? '',
|
||||
createdOn: chatMessage.createdOn,
|
||||
senderId: chatMessage.sender?.communicationUserId,
|
||||
senderDisplayName: chatMessage.senderDisplayName,
|
||||
// clientMessageId field is attached by useSendMessage hooks,
|
||||
// and it's needed to filter out failed messages, will not used by MessageThread component.
|
||||
clientMessageId: chatMessage.clientMessageId
|
||||
};
|
||||
}) ?? [];
|
||||
return convertedChatMessages.length > 0
|
||||
? updateMessagesWithAttached(convertedChatMessages, userId, failedMessageIds, isLargeGroup, isMessageSeen)
|
||||
: [];
|
||||
};
|
||||
|
||||
export type ChatMessagePropsFromContext = {
|
||||
userId: string;
|
||||
messages: ChatMessage[];
|
||||
disableReadReceipt?: boolean;
|
||||
onSendReadReceipt?: () => Promise<void>;
|
||||
disableLoadPreviousMessage?: boolean;
|
||||
onLoadPreviousMessages?: () => void;
|
||||
onRenderAvatar?: (userId: string) => JSX.Element;
|
||||
};
|
||||
|
||||
export const MapToChatMessageProps = (): ChatMessagePropsFromContext => {
|
||||
useSubscribeReadReceipt();
|
||||
useSubscribeMessage();
|
||||
const sdkChatMessages = useChatMessages();
|
||||
const failedMessageIds = useFailedMessageIds();
|
||||
const threadMembers = useThreadMembers();
|
||||
const isMessageSeen = useIsMessageSeen();
|
||||
const userId = useUserId();
|
||||
const isLargeGroup = useMemo(() => {
|
||||
return isLargeParticipantsGroup(threadMembers);
|
||||
}, [threadMembers]);
|
||||
const sendReadReceipt = useSendReadReceipt();
|
||||
const [messagesNumber, setMessagesNumber] = useState<number>(25);
|
||||
const [disableLoadPreviousMessage, setDisableLoadPreviousMessage] = useState<boolean>(false);
|
||||
const chatMessages = useMemo(() => {
|
||||
sdkChatMessages && messagesNumber >= sdkChatMessages.length
|
||||
? !disableLoadPreviousMessage && setDisableLoadPreviousMessage(true)
|
||||
: disableLoadPreviousMessage && setDisableLoadPreviousMessage(false);
|
||||
return convertSdkChatMessagesToWebUiChatMessages(
|
||||
sdkChatMessages?.slice(Math.max(sdkChatMessages.length - messagesNumber, 0)) ?? [],
|
||||
failedMessageIds,
|
||||
isLargeGroup,
|
||||
userId,
|
||||
isMessageSeen
|
||||
);
|
||||
}, [
|
||||
failedMessageIds,
|
||||
isLargeGroup,
|
||||
isMessageSeen,
|
||||
sdkChatMessages,
|
||||
userId,
|
||||
messagesNumber,
|
||||
disableLoadPreviousMessage
|
||||
]);
|
||||
|
||||
const onSendReadReceipt = useCallback(async () => {
|
||||
const messageId = getLatestIncomingMessageId(chatMessages, userId);
|
||||
messageId && (await sendReadReceipt(messageId));
|
||||
}, [chatMessages, userId, sendReadReceipt]);
|
||||
|
||||
const fetchMessages = useFetchMessages();
|
||||
useEffect(() => {
|
||||
fetchMessages({ maxPageSize: PAGE_SIZE });
|
||||
}, [fetchMessages]);
|
||||
|
||||
const onLoadPreviousMessages: () => void = useCallback(() => {
|
||||
setMessagesNumber(messagesNumber + 10);
|
||||
}, [messagesNumber]);
|
||||
|
||||
return {
|
||||
userId: userId,
|
||||
messages: chatMessages,
|
||||
disableReadReceipt: isLargeGroup,
|
||||
onSendReadReceipt,
|
||||
disableLoadPreviousMessage,
|
||||
onLoadPreviousMessages
|
||||
};
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
import {
|
||||
useGetThreadMembersError,
|
||||
useGetUpdateThreadMembersError,
|
||||
useSetUpdateThreadMembersError
|
||||
} from '../providers/ChatThreadProvider';
|
||||
|
||||
export type ErrorsPropsFromContext = {
|
||||
getThreadMembersError: boolean | undefined;
|
||||
updateThreadMembersError: boolean | undefined;
|
||||
setUpdateThreadMembersError: (error: boolean | undefined) => void;
|
||||
};
|
||||
|
||||
export const MapToErrorsProps = (): ErrorsPropsFromContext => {
|
||||
const getThreadMembersError = useGetThreadMembersError();
|
||||
const updateThreadMembersError = useGetUpdateThreadMembersError();
|
||||
return {
|
||||
getThreadMembersError: getThreadMembersError,
|
||||
updateThreadMembersError: updateThreadMembersError,
|
||||
setUpdateThreadMembersError: useSetUpdateThreadMembersError()
|
||||
};
|
||||
};
|
|
@ -7,6 +7,4 @@ export * from './MapToCallConfigurationProps';
|
|||
export * from './MapToVideoProps';
|
||||
export * from './MapToLocalDeviceSettingsProps';
|
||||
|
||||
export * from './MapToChatMessageProps';
|
||||
export * from './MapToErrorsProps';
|
||||
export * from './MapToErrorBarProps';
|
||||
|
|
|
@ -21,24 +21,8 @@ export {
|
|||
useSubscribeToVideoDeviceList
|
||||
};
|
||||
|
||||
export { useFetchMessage } from './useFetchMessage';
|
||||
export { useFetchMessages } from './useFetchMessages';
|
||||
export { useFetchReadReceipts } from './useFetchReadReceipts';
|
||||
export { useFetchThread } from './useFetchThread';
|
||||
export { useFetchThreadMembers } from './useFetchThreadMembers';
|
||||
export { useGroupCall } from './useGroupCall';
|
||||
export { useIsMessageSeen } from './useIsMessageSeen';
|
||||
export { useMicrophone } from './useMicrophone';
|
||||
export { useOutgoingCall } from './useOutgoingCall';
|
||||
export { useRemoveThreadMember } from './useRemoveThreadMember';
|
||||
export { useSendMessage } from './useSendMessage';
|
||||
export type { ChatMessageWithClientMessageId } from './useSendMessage';
|
||||
export { useSendReadReceipt } from './useSendReadReceipt';
|
||||
export { useSendTypingNotification } from './useSendTypingNotification';
|
||||
export { useSubscribeMessage } from './useSubscribeMessage';
|
||||
export { useSubscribeReadReceipt } from './useSubscribeReadReceipt';
|
||||
export { useSubscribeTypingNotification } from './useSubscribeTypingNotification';
|
||||
export { useTeamsCall } from './useTeamsCall';
|
||||
export { useTypingUsers } from './useTypingUsers';
|
||||
export { useUpdateThreadTopicName } from './useUpdateThreadTopicName';
|
||||
export { useIncomingCall } from './useIncomingCall';
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { OK, TEXT_MESSAGE } from '../constants';
|
||||
import { ThreadClientMock, createThreadClient } from '../mocks/ChatThreadClientMocks';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { ChatMessage } from '@azure/communication-chat';
|
||||
import { useFetchMessage } from './useFetchMessage';
|
||||
|
||||
let threadClientMock: ThreadClientMock;
|
||||
|
||||
type ResponseWithStatus = {
|
||||
status: number;
|
||||
};
|
||||
|
||||
type ChatMessageWithResponseStatus = {
|
||||
_response: ResponseWithStatus;
|
||||
} & ChatMessage;
|
||||
|
||||
export const mockChatMessageWithResponse = (): ChatMessageWithResponseStatus => {
|
||||
return {
|
||||
_response: { status: OK },
|
||||
id: '1',
|
||||
content: { message: '1' },
|
||||
type: TEXT_MESSAGE,
|
||||
sequenceId: '',
|
||||
createdOn: new Date(),
|
||||
version: ''
|
||||
};
|
||||
};
|
||||
|
||||
export const mockChatMessage = (): ChatMessage => {
|
||||
return {
|
||||
id: '1',
|
||||
content: { message: '1' },
|
||||
type: TEXT_MESSAGE,
|
||||
sequenceId: '',
|
||||
createdOn: new Date(0),
|
||||
version: ''
|
||||
};
|
||||
};
|
||||
|
||||
const mockThreadClient = (): ThreadClientMock => {
|
||||
threadClientMock = createThreadClient();
|
||||
return threadClientMock;
|
||||
};
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useChatThreadClient: jest.fn().mockImplementation(
|
||||
(): ThreadClientMock => {
|
||||
return mockThreadClient();
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
describe('useFetchMessage tests', () => {
|
||||
test('should be able to call useFetchMessage to get the correct messages', async (): Promise<void> => {
|
||||
const expectedMessage: ChatMessage = mockChatMessage();
|
||||
const { result } = renderHook(() => useFetchMessage());
|
||||
let message: ChatMessage | undefined;
|
||||
await act(async () => {
|
||||
message = await result.current('1');
|
||||
});
|
||||
expect(threadClientMock.getMessage).toBeCalledTimes(1);
|
||||
expect(message).toMatchObject(expectedMessage);
|
||||
});
|
||||
});
|
|
@ -1,52 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatMessage, ChatThreadClient, GetChatMessageResponse } from '@azure/communication-chat';
|
||||
import { useCallback } from 'react';
|
||||
import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError';
|
||||
import { OK, TOO_MANY_REQUESTS_STATUS_CODE } from '../constants';
|
||||
|
||||
import { useChatThreadClient } from '../providers/ChatThreadProvider';
|
||||
|
||||
const fetchMessageInternal = async (
|
||||
chatThreadClient: ChatThreadClient,
|
||||
messageId: string
|
||||
): Promise<ChatMessage | undefined> => {
|
||||
let messageResponse: GetChatMessageResponse;
|
||||
try {
|
||||
messageResponse = await chatThreadClient.getMessage(messageId);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error getting message',
|
||||
code: CommunicationUiErrorCode.GET_MESSAGE_ERROR,
|
||||
error
|
||||
});
|
||||
}
|
||||
if (messageResponse._response.status === OK) {
|
||||
const { _response, ...chatMessage } = messageResponse;
|
||||
return chatMessage;
|
||||
} else if (messageResponse._response.status === TOO_MANY_REQUESTS_STATUS_CODE) {
|
||||
// TODO: looks like these console.errors are ok to happen so we don't throw but we should replace these with logger
|
||||
console.error('Failed at fetching message, Error: Too many requests');
|
||||
return undefined;
|
||||
} else {
|
||||
console.error('Failed at fetching message, Error: ', messageResponse._response.status);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const useFetchMessage = (): ((messageId: string) => Promise<ChatMessage | undefined>) => {
|
||||
const chatThreadClient = useChatThreadClient();
|
||||
if (!chatThreadClient) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
const fetchMessage = useCallback(
|
||||
async (messageId: string): Promise<ChatMessage | undefined> => {
|
||||
return await fetchMessageInternal(chatThreadClient, messageId);
|
||||
},
|
||||
[chatThreadClient]
|
||||
);
|
||||
return fetchMessage;
|
||||
};
|
|
@ -1,50 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ThreadClientMock, createThreadClient, mockChatMessages } from '../mocks/ChatThreadClientMocks';
|
||||
|
||||
import { ChatMessage } from '@azure/communication-chat';
|
||||
import { PAGE_SIZE } from '../constants';
|
||||
import { useFetchMessages } from './useFetchMessages';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
let threadClientMock: ThreadClientMock;
|
||||
|
||||
const mockThreadClient = (): ThreadClientMock => {
|
||||
threadClientMock = createThreadClient();
|
||||
return threadClientMock;
|
||||
};
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useChatThreadClient: jest.fn().mockImplementation(
|
||||
(): ThreadClientMock => {
|
||||
return mockThreadClient();
|
||||
}
|
||||
),
|
||||
useSetChatMessages: jest.fn(() => jest.fn())
|
||||
};
|
||||
});
|
||||
|
||||
describe('useFetchMessages tests', () => {
|
||||
test('should be able to call useFetchMessages', async (): Promise<void> => {
|
||||
renderHook(() => {
|
||||
const fetchMessages = useFetchMessages();
|
||||
if (fetchMessages !== undefined) fetchMessages({ maxPageSize: PAGE_SIZE });
|
||||
});
|
||||
|
||||
expect(threadClientMock.listMessages).toBeCalledTimes(1);
|
||||
});
|
||||
test('should be able to call useFetchMessages to get the correct messages', async (): Promise<void> => {
|
||||
const expectedMessages: ChatMessage[] = mockChatMessages().reverse();
|
||||
let messages: ChatMessage[] = [];
|
||||
const {
|
||||
result: { current: fetchMessages }
|
||||
} = renderHook(() => useFetchMessages());
|
||||
|
||||
await act(async () => {
|
||||
messages = await fetchMessages({ maxPageSize: PAGE_SIZE });
|
||||
});
|
||||
|
||||
expect(messages).toMatchObject(expectedMessages);
|
||||
});
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatMessage, ChatThreadClient, ListMessagesOptions } from '@azure/communication-chat';
|
||||
import { useChatThreadClient, useSetChatMessages } from '../providers/ChatThreadProvider';
|
||||
|
||||
import { TEXT_MESSAGE } from '../constants';
|
||||
import { useCallback } from 'react';
|
||||
import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError';
|
||||
|
||||
// The following need explicitly imported to avoid api-extractor issues.
|
||||
// These can be removed once https://github.com/microsoft/rushstack/pull/1916 is fixed.
|
||||
// @ts-ignore
|
||||
import { RestListMessagesOptions } from '@azure/communication-chat';
|
||||
|
||||
const fetchMessagesInternal = async (
|
||||
chatThreadClient: ChatThreadClient,
|
||||
options?: ListMessagesOptions
|
||||
): Promise<ChatMessage[]> => {
|
||||
let messages: ChatMessage[] = [];
|
||||
let getMessagesResponse;
|
||||
try {
|
||||
getMessagesResponse = chatThreadClient.listMessages(options);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error getting messages',
|
||||
code: CommunicationUiErrorCode.GET_MESSAGES_ERROR
|
||||
});
|
||||
}
|
||||
for await (const message of getMessagesResponse) {
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// filter to only text messages
|
||||
messages = messages.filter((message) => message.type === TEXT_MESSAGE);
|
||||
return messages.reverse();
|
||||
};
|
||||
|
||||
export const useFetchMessages = (): ((options?: ListMessagesOptions) => Promise<ChatMessage[]>) => {
|
||||
const chatThreadClient = useChatThreadClient();
|
||||
const setChatMessages = useSetChatMessages();
|
||||
if (!chatThreadClient) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
const fetchMessages = useCallback(
|
||||
async (options?: ListMessagesOptions): Promise<ChatMessage[]> => {
|
||||
const messages = await fetchMessagesInternal(chatThreadClient, options);
|
||||
setChatMessages(messages);
|
||||
return messages;
|
||||
},
|
||||
[chatThreadClient, setChatMessages]
|
||||
);
|
||||
return fetchMessages;
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatMessageReadReceipt } from '@azure/communication-chat';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
CommunicationUiErrorCode,
|
||||
CommunicationUiError,
|
||||
CommunicationUiErrorSeverity
|
||||
} from '../types/CommunicationUiError';
|
||||
import { useChatThreadClient } from '../providers/ChatThreadProvider';
|
||||
|
||||
export const useFetchReadReceipts = (): (() => Promise<ChatMessageReadReceipt[]>) => {
|
||||
const chatThreadClient = useChatThreadClient();
|
||||
if (chatThreadClient === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.IGNORE
|
||||
});
|
||||
}
|
||||
const fetchReadReceipts = useCallback(async (): Promise<ChatMessageReadReceipt[]> => {
|
||||
const receipts: ChatMessageReadReceipt[] = [];
|
||||
try {
|
||||
for await (const page of chatThreadClient.listReadReceipts().byPage()) {
|
||||
for (const receipt of page) {
|
||||
receipts.push(receipt);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error getting read receipts',
|
||||
code: CommunicationUiErrorCode.GET_READ_RECEIPT_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.IGNORE,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
|
||||
return receipts;
|
||||
}, [chatThreadClient]);
|
||||
return fetchReadReceipts;
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatThread } from '@azure/communication-chat';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { act } from 'react-test-renderer';
|
||||
import { ChatClientMock, createChatClient } from '../mocks/ChatClientMocks';
|
||||
|
||||
import { useFetchThread } from './useFetchThread';
|
||||
|
||||
const mockChatClient = (): ChatClientMock => {
|
||||
return createChatClient();
|
||||
};
|
||||
|
||||
jest.mock('../providers/ChatProviderHelper', () => {
|
||||
return {
|
||||
useChatClient: jest.fn().mockImplementation(
|
||||
(): ChatClientMock => {
|
||||
return mockChatClient();
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
let threadToPass: ChatThread | undefined = undefined;
|
||||
const mockSetThread = (thread: ChatThread): void => {
|
||||
threadToPass = thread;
|
||||
};
|
||||
const expectThread = { createdBy: { communicationUserId: 'userId' } };
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useThreadId: jest.fn(() => jest.fn()),
|
||||
useSetThread: () => mockSetThread
|
||||
};
|
||||
});
|
||||
|
||||
describe('useFetchThread tests', () => {
|
||||
test('should be able to call useFetchThread', async (): Promise<void> => {
|
||||
let fetchThread: (() => Promise<void>) | undefined = undefined;
|
||||
renderHook(() => {
|
||||
fetchThread = useFetchThread();
|
||||
});
|
||||
await act(async () => {
|
||||
if (fetchThread !== undefined) await fetchThread();
|
||||
});
|
||||
expect(threadToPass).toMatchObject(expectThread);
|
||||
});
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { useSetThread, useThreadId } from '../providers/ChatThreadProvider';
|
||||
import { useChatClient } from '../providers/ChatProviderHelper';
|
||||
import { useCallback } from 'react';
|
||||
import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError';
|
||||
|
||||
export const useFetchThread = (): (() => Promise<void>) => {
|
||||
const chatClient = useChatClient();
|
||||
const threadId = useThreadId();
|
||||
if (threadId === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ThreadId is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
const setThread = useSetThread();
|
||||
const useFetchThreadInternal = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const thread = await chatClient.getChatThread(threadId);
|
||||
setThread(thread);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error getting chat thread',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
}, [chatClient, threadId, setThread]);
|
||||
return useFetchThreadInternal;
|
||||
};
|
|
@ -1,71 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatParticipant } from '@azure/communication-chat';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { ThreadClientMock, createThreadClient } from '../mocks/ChatThreadClientMocks';
|
||||
|
||||
import { useFetchThreadMembers } from './useFetchThreadMembers';
|
||||
|
||||
const mockCurrentUser = 'userId1';
|
||||
|
||||
const threadMembersWithCurrentUser = [
|
||||
{ user: { communicationUserId: mockCurrentUser } },
|
||||
{ user: { communicationUserId: 'userId2' } },
|
||||
{ user: { communicationUserId: 'userId3' } }
|
||||
];
|
||||
|
||||
let threadMembersMock: ChatParticipant[] = [];
|
||||
|
||||
let threadMembersToPass: ChatParticipant[] = [];
|
||||
const mockSetThreadMembers = (threadMembers: ChatParticipant[]): void => {
|
||||
threadMembersToPass = threadMembers;
|
||||
};
|
||||
|
||||
const mockThreadClient = (): ThreadClientMock => {
|
||||
const threadClientMock: ThreadClientMock = createThreadClient();
|
||||
threadClientMock.listParticipants = () => threadMembersMock;
|
||||
return threadClientMock;
|
||||
};
|
||||
const mockUseSetGetThreadMembersError: (value: boolean) => void = jest.fn();
|
||||
const mockUseUpdateGetThreadMembersError: (value: boolean) => void = jest.fn();
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useChatThreadClient: jest.fn().mockImplementation(
|
||||
(): ThreadClientMock => {
|
||||
return mockThreadClient();
|
||||
}
|
||||
),
|
||||
useSetUpdateThreadMembersError: jest.fn(() => mockUseUpdateGetThreadMembersError),
|
||||
useSetThreadMembers: () => mockSetThreadMembers,
|
||||
useThreadId: () => 'abcde',
|
||||
useSetGetThreadMembersError: jest.fn(() => mockUseSetGetThreadMembersError)
|
||||
};
|
||||
});
|
||||
|
||||
describe('useFetchThreadMembers tests', () => {
|
||||
test('should be able to call useFetchThreadMembers', async (): Promise<void> => {
|
||||
threadMembersMock = threadMembersWithCurrentUser;
|
||||
let fetchThreadMembers: (() => Promise<void>) | undefined = undefined;
|
||||
renderHook(() => {
|
||||
fetchThreadMembers = useFetchThreadMembers();
|
||||
});
|
||||
await act(async () => {
|
||||
if (fetchThreadMembers !== undefined) await fetchThreadMembers();
|
||||
});
|
||||
expect(threadMembersToPass).toMatchObject(threadMembersWithCurrentUser);
|
||||
});
|
||||
test('should mark thread error when fail to fetch members from both listMembers and getThread', async (): Promise<
|
||||
void
|
||||
> => {
|
||||
threadMembersMock = [];
|
||||
let fetchThreadMembers: (() => Promise<void>) | undefined = undefined;
|
||||
renderHook(() => {
|
||||
fetchThreadMembers = useFetchThreadMembers();
|
||||
});
|
||||
await act(async () => {
|
||||
if (fetchThreadMembers !== undefined) await fetchThreadMembers();
|
||||
});
|
||||
expect(mockUseUpdateGetThreadMembersError).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatParticipant } from '@azure/communication-chat';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
useChatThreadClient,
|
||||
useSetThreadMembers,
|
||||
useSetUpdateThreadMembersError
|
||||
} from '../providers/ChatThreadProvider';
|
||||
import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError';
|
||||
|
||||
// TODO: A lot of the code here is specific to Sample App such as the 'setThreadMembersError' which is used to show
|
||||
// the 'you have been removed' screen in Sample App. This file requires some refactoring.
|
||||
export const useFetchThreadMembers = (): (() => Promise<void>) => {
|
||||
const chatThreadClient = useChatThreadClient();
|
||||
const setUpdateThreadMembersError = useSetUpdateThreadMembersError();
|
||||
const setThreadMembers = useSetThreadMembers();
|
||||
|
||||
if (chatThreadClient === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
const useFetchThreadMembersInternal = useCallback(async (): Promise<void> => {
|
||||
const threadMembers: ChatParticipant[] = [];
|
||||
const getThreadMembersResponse = chatThreadClient.listParticipants();
|
||||
for await (const threadMember of getThreadMembersResponse) {
|
||||
// TODO: fix typescript error
|
||||
threadMembers.push(threadMember);
|
||||
}
|
||||
if (threadMembers.length === 0) {
|
||||
console.error('unable to get members in the thread');
|
||||
setUpdateThreadMembersError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: fix typescript error
|
||||
setThreadMembers(threadMembers as any);
|
||||
}, [chatThreadClient, setThreadMembers, setUpdateThreadMembersError]);
|
||||
return useFetchThreadMembersInternal;
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useReceipts } from '../providers/ChatThreadProvider';
|
||||
import { ChatMessagePayload } from '../types';
|
||||
|
||||
export const useIsMessageSeen = (): ((userId: string, message: ChatMessagePayload) => boolean) => {
|
||||
const receipts = useReceipts();
|
||||
|
||||
const internal = useCallback(
|
||||
(userId: string, message: ChatMessagePayload): boolean => {
|
||||
if (!receipts || receipts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const numSeen = receipts?.filter((receipt) => {
|
||||
if ((receipt.sender?.communicationUserId as string) === userId) {
|
||||
//don't count sender's own read receipt
|
||||
return false;
|
||||
}
|
||||
return new Date(receipt.readOn ?? -1) >= new Date(message.createdOn ?? -1);
|
||||
}).length;
|
||||
return numSeen > 0 ? true : false;
|
||||
},
|
||||
[receipts]
|
||||
);
|
||||
|
||||
return internal;
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { useChatThreadClient } from '../providers/ChatThreadProvider';
|
||||
import { useCallback } from 'react';
|
||||
import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError';
|
||||
import { getErrorFromAcsResponseCode } from '../utils/SDKUtils';
|
||||
|
||||
export const useRemoveThreadMember = (): ((userId: string) => Promise<void>) => {
|
||||
const chatThreadClient = useChatThreadClient();
|
||||
const useRemoveThreadMemberInternal = useCallback(
|
||||
async (userId: string): Promise<void> => {
|
||||
if (chatThreadClient === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await chatThreadClient.removeParticipant({
|
||||
communicationUserId: userId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error removing thread member',
|
||||
code: CommunicationUiErrorCode.REMOVE_THREAD_MEMBER_ERROR,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
const error = getErrorFromAcsResponseCode(
|
||||
'Error removing thread member, status code: ',
|
||||
response._response.status
|
||||
);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[chatThreadClient]
|
||||
);
|
||||
return useRemoveThreadMemberInternal;
|
||||
};
|
|
@ -1,184 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
import { BaseClientMock, createBaseClientMock } from '../mocks/ChatClientMocks';
|
||||
import {
|
||||
CREATED,
|
||||
MAXIMUM_RETRY_COUNT,
|
||||
OK,
|
||||
PRECONDITION_FAILED_STATUS_CODE,
|
||||
TOO_MANY_REQUESTS_STATUS_CODE
|
||||
} from '../constants';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { SendChatMessageResult } from '@azure/communication-chat';
|
||||
import { useSendMessage } from './useSendMessage';
|
||||
import { CommunicationUiError } from '../types/CommunicationUiError';
|
||||
|
||||
/**
|
||||
* Unfortunately could not just mock single exported variable in chatConstants in Jest so we have to mock every since
|
||||
* constant used by useSendMessage. If you find this test breaking and you made a change to chatConstants see if this
|
||||
* mock has values that needs to be updated. If you find this test breaking and you made a change to useSendMessage see
|
||||
* if this mock needs to be updated with any new constants.
|
||||
*/
|
||||
jest.mock('../constants/chatConstants', () => {
|
||||
return {
|
||||
COOL_PERIOD_THRESHOLD: 1,
|
||||
PRECONDITION_FAILED_RETRY_INTERVAL: 1,
|
||||
CREATED: 201,
|
||||
MAXIMUM_INT64: 9223372036854775807,
|
||||
MAXIMUM_RETRY_COUNT: 3,
|
||||
PRECONDITION_FAILED_STATUS_CODE: 412,
|
||||
TOO_MANY_REQUESTS_STATUS_CODE: 429
|
||||
};
|
||||
});
|
||||
|
||||
export type ThreadClientMock = {
|
||||
getMessage: () => void;
|
||||
sendMessage: () => void;
|
||||
} & BaseClientMock;
|
||||
|
||||
type SendMessageResponseWithStatus = {
|
||||
_response: ResponseWithStatus;
|
||||
} & SendChatMessageResult;
|
||||
|
||||
type ResponseWithStatus = {
|
||||
status: number;
|
||||
};
|
||||
|
||||
const UNIT_TEST_THROW_ERROR_FLAG = 99999;
|
||||
let sendMessageResponseStatus: number;
|
||||
|
||||
const mockSendMessageWithCreatedResponse = (): SendMessageResponseWithStatus => {
|
||||
return { _response: { status: CREATED }, id: '1' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Note current behavior of TOO_MANY_REQUESTS is to resend the message after a cool down period. This means in a test
|
||||
* scenario where we alreays return TOO_MANY_REQUESTS this leads to infinite loop. So this counter is added to break
|
||||
* the infinite loop by stop returning TOO_MANY_REQUESTS. Maybe we should look into having a MAX_RETRY similar to
|
||||
* PRECONDITION_FAILED scenario.
|
||||
*/
|
||||
let tooManyRequestsCounter = 0;
|
||||
const mockSendMessageWithTooManyRequestsResponse = (): SendMessageResponseWithStatus => {
|
||||
if (tooManyRequestsCounter < 1) {
|
||||
tooManyRequestsCounter++;
|
||||
return { _response: { status: TOO_MANY_REQUESTS_STATUS_CODE }, id: '' };
|
||||
} else {
|
||||
return { _response: { status: CREATED }, id: '1' };
|
||||
}
|
||||
};
|
||||
|
||||
const mockSendMessageWithPreConditionFaileResponse = (): SendMessageResponseWithStatus => {
|
||||
return { _response: { status: PRECONDITION_FAILED_STATUS_CODE }, id: '' };
|
||||
};
|
||||
|
||||
const mockSendMessageWithThrowErrorResponse = (): SendMessageResponseWithStatus => {
|
||||
throw new Error('disconnected in unit test');
|
||||
};
|
||||
|
||||
export const createThreadClient = (): ThreadClientMock => {
|
||||
const clientMock = createBaseClientMock();
|
||||
let sendMessageResponse: () => SendMessageResponseWithStatus;
|
||||
switch (sendMessageResponseStatus) {
|
||||
case OK:
|
||||
sendMessageResponse = mockSendMessageWithCreatedResponse;
|
||||
break;
|
||||
case TOO_MANY_REQUESTS_STATUS_CODE:
|
||||
sendMessageResponse = mockSendMessageWithTooManyRequestsResponse;
|
||||
break;
|
||||
case PRECONDITION_FAILED_STATUS_CODE:
|
||||
sendMessageResponse = mockSendMessageWithPreConditionFaileResponse;
|
||||
break;
|
||||
case UNIT_TEST_THROW_ERROR_FLAG:
|
||||
sendMessageResponse = mockSendMessageWithThrowErrorResponse;
|
||||
break;
|
||||
default:
|
||||
sendMessageResponse = mockSendMessageWithCreatedResponse;
|
||||
break;
|
||||
}
|
||||
|
||||
const threadClientMock: ThreadClientMock = {
|
||||
...clientMock,
|
||||
getMessage: jest.fn(),
|
||||
sendMessage: jest.fn(sendMessageResponse)
|
||||
};
|
||||
return threadClientMock;
|
||||
};
|
||||
|
||||
let threadClientMock: ThreadClientMock;
|
||||
|
||||
const mockThreadClient = (): ThreadClientMock => {
|
||||
threadClientMock = createThreadClient();
|
||||
return threadClientMock;
|
||||
};
|
||||
|
||||
const mockSetFailedMessageIds = jest.fn();
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useChatThreadClient: jest.fn((): ThreadClientMock => mockThreadClient()),
|
||||
useThreadId: jest.fn(() => 'mockThreadId'),
|
||||
useSetChatMessages: jest.fn(() => jest.fn()),
|
||||
useSetCoolPeriod: jest.fn(() => jest.fn()),
|
||||
useSetFailedMessageIds: jest.fn(() => mockSetFailedMessageIds),
|
||||
useFailedMessageIds: jest.fn(() => [])
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./useFetchMessage', () => {
|
||||
return {
|
||||
useFetchMessage: jest.fn(() => jest.fn())
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockSetFailedMessageIds.mockClear();
|
||||
});
|
||||
|
||||
describe('useSendMessage tests', () => {
|
||||
test('should be able to call useSendMessage', async () => {
|
||||
sendMessageResponseStatus = OK;
|
||||
const { result } = renderHook(() => useSendMessage('mockDisplayName', 'mockUserId'));
|
||||
await result.current('mockMessage');
|
||||
expect(threadClientMock.sendMessage).toBeCalledTimes(1);
|
||||
expect(threadClientMock.sendMessage).toBeCalledWith(expect.objectContaining({ content: 'mockMessage' }));
|
||||
expect(mockSetFailedMessageIds).toBeCalledTimes(0);
|
||||
});
|
||||
test('should be able to retry sending message for MAXIMUM_RETRY_COUNT times after encountering PRECONDITION_FAILED_STATUS_CODE status and after max retry throw an error', async () => {
|
||||
sendMessageResponseStatus = PRECONDITION_FAILED_STATUS_CODE;
|
||||
const { result } = renderHook(() => useSendMessage('mockDisplayName', 'mockUserId'));
|
||||
let caughtError;
|
||||
try {
|
||||
await result.current('mockMessage');
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
}
|
||||
expect(threadClientMock.sendMessage).toBeCalledTimes(MAXIMUM_RETRY_COUNT + 1);
|
||||
expect(threadClientMock.sendMessage).toBeCalledWith(expect.objectContaining({ content: 'mockMessage' }));
|
||||
expect(caughtError).toBeDefined();
|
||||
expect(caughtError instanceof CommunicationUiError).toBe(true);
|
||||
});
|
||||
test('should be able to call retry sending message after COOL_PERIOD_THRESHOLD', async () => {
|
||||
sendMessageResponseStatus = TOO_MANY_REQUESTS_STATUS_CODE;
|
||||
tooManyRequestsCounter = 0;
|
||||
const { result } = renderHook(() => useSendMessage('mockDisplayName', 'mockUserId'));
|
||||
await result.current('mockMessage');
|
||||
expect(threadClientMock.sendMessage).toBeCalledTimes(tooManyRequestsCounter + 1);
|
||||
expect(threadClientMock.sendMessage).toBeCalledWith(expect.objectContaining({ content: 'mockMessage' }));
|
||||
});
|
||||
test('should throw an error and update failedIds if chat client send message threw an error', async () => {
|
||||
sendMessageResponseStatus = UNIT_TEST_THROW_ERROR_FLAG;
|
||||
const { result } = renderHook(() => useSendMessage('mockDisplayName', 'mockUserId'));
|
||||
let caughtError;
|
||||
try {
|
||||
await result.current('mockDisplayName', 'mockUserId', 'mockMessage');
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
}
|
||||
expect(threadClientMock.sendMessage).toBeCalledTimes(1);
|
||||
expect(mockSetFailedMessageIds).toBeCalledTimes(1);
|
||||
expect(caughtError).toBeDefined();
|
||||
expect(caughtError instanceof CommunicationUiError).toBe(true);
|
||||
});
|
||||
});
|
|
@ -1,270 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import {
|
||||
COOL_PERIOD_THRESHOLD,
|
||||
CREATED,
|
||||
MAXIMUM_INT64,
|
||||
MAXIMUM_RETRY_COUNT,
|
||||
PRECONDITION_FAILED_RETRY_INTERVAL,
|
||||
PRECONDITION_FAILED_STATUS_CODE,
|
||||
TOO_MANY_REQUESTS_STATUS_CODE
|
||||
} from '../constants';
|
||||
import { ChatMessage, ChatThreadClient } from '@azure/communication-chat';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
useChatThreadClient,
|
||||
useSetChatMessages,
|
||||
useSetCoolPeriod,
|
||||
useThreadId
|
||||
} from '../providers/ChatThreadProvider';
|
||||
|
||||
import { compareMessages } from '../utils/chatUtils';
|
||||
import { useFetchMessage } from './useFetchMessage';
|
||||
import { useFailedMessageIds, useSetFailedMessageIds } from '../providers/ChatThreadProvider';
|
||||
import { useCallback } from 'react';
|
||||
import { getErrorFromAcsResponseCode } from '../utils/SDKUtils';
|
||||
import {
|
||||
CommunicationUiErrorCode,
|
||||
CommunicationUiError,
|
||||
CommunicationUiErrorSeverity
|
||||
} from '../types/CommunicationUiError';
|
||||
|
||||
export interface ChatMessageWithClientMessageId extends ChatMessage {
|
||||
clientMessageId?: string;
|
||||
}
|
||||
|
||||
const updateMessagesArray = (
|
||||
newMessage: ChatMessageWithClientMessageId,
|
||||
setChatMessages: Dispatch<SetStateAction<ChatMessage[] | undefined>>
|
||||
): void => {
|
||||
setChatMessages((prevMessages) => {
|
||||
let messages: ChatMessage[] = prevMessages ? [...prevMessages] : [];
|
||||
messages = messages.map((message: ChatMessageWithClientMessageId) => {
|
||||
if (message.clientMessageId === newMessage.clientMessageId) {
|
||||
return {
|
||||
...message,
|
||||
...newMessage
|
||||
};
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
});
|
||||
return messages.sort(compareMessages);
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessageWithClient = async (
|
||||
chatThreadClient: ChatThreadClient,
|
||||
messageContent: string,
|
||||
displayName: string,
|
||||
clientMessageId: string,
|
||||
retryCount: number,
|
||||
setChatMessages: Dispatch<SetStateAction<ChatMessage[] | undefined>>,
|
||||
getMessage: (messageId: string) => Promise<ChatMessage | undefined>,
|
||||
setCoolPeriod: (coolPeriod: Date) => void,
|
||||
failedMessageIds: string[],
|
||||
setFailedMessageIds: Dispatch<SetStateAction<string[]>>
|
||||
): Promise<void> => {
|
||||
const sendMessageRequest = {
|
||||
content: messageContent,
|
||||
senderDisplayName: displayName
|
||||
};
|
||||
try {
|
||||
let sendMessageResponse;
|
||||
try {
|
||||
sendMessageResponse = await chatThreadClient.sendMessage(sendMessageRequest);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error sending message',
|
||||
code: CommunicationUiErrorCode.SEND_MESSAGE_ERROR,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
if (sendMessageResponse._response.status === CREATED) {
|
||||
if (sendMessageResponse.id) {
|
||||
let message;
|
||||
try {
|
||||
message = await getMessage(sendMessageResponse.id);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error getting message',
|
||||
code: CommunicationUiErrorCode.GET_MESSAGE_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.IGNORE,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
if (message) {
|
||||
updateMessagesArray({ ...message, clientMessageId }, setChatMessages);
|
||||
} else {
|
||||
updateMessagesArray(
|
||||
{
|
||||
clientMessageId: clientMessageId,
|
||||
createdOn: new Date(),
|
||||
id: sendMessageResponse.id,
|
||||
type: 'text',
|
||||
sequenceId: '',
|
||||
version: ''
|
||||
},
|
||||
setChatMessages
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (sendMessageResponse._response.status === TOO_MANY_REQUESTS_STATUS_CODE) {
|
||||
setCoolPeriod(new Date());
|
||||
// retry after cool period
|
||||
|
||||
// We are awaiting the setTimeout and then also the sendMessageWithClient so it doesn't execute in separate async
|
||||
// function and will execute inside the current async function so if sendMessageWithClient throws it can be caught
|
||||
// in the surrounding try catch.
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, COOL_PERIOD_THRESHOLD);
|
||||
});
|
||||
|
||||
await sendMessageWithClient(
|
||||
chatThreadClient,
|
||||
messageContent,
|
||||
displayName,
|
||||
clientMessageId,
|
||||
retryCount,
|
||||
setChatMessages,
|
||||
getMessage,
|
||||
setCoolPeriod,
|
||||
failedMessageIds,
|
||||
setFailedMessageIds
|
||||
);
|
||||
} else if (sendMessageResponse._response.status === PRECONDITION_FAILED_STATUS_CODE) {
|
||||
if (retryCount >= MAXIMUM_RETRY_COUNT) {
|
||||
setFailedMessageIds((failedMessageIds) => {
|
||||
return [...failedMessageIds, clientMessageId];
|
||||
});
|
||||
throw new CommunicationUiError({
|
||||
message: 'Failed at sending message and reached max retry count',
|
||||
code: CommunicationUiErrorCode.MESSAGE_EXCEEDED_RETRY_ERROR
|
||||
});
|
||||
}
|
||||
// retry in 0.2s
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, PRECONDITION_FAILED_RETRY_INTERVAL);
|
||||
});
|
||||
|
||||
await sendMessageWithClient(
|
||||
chatThreadClient,
|
||||
messageContent,
|
||||
displayName,
|
||||
clientMessageId,
|
||||
retryCount + 1,
|
||||
setChatMessages,
|
||||
getMessage,
|
||||
setCoolPeriod,
|
||||
failedMessageIds,
|
||||
setFailedMessageIds
|
||||
);
|
||||
} else {
|
||||
setFailedMessageIds((failedMessageIds) => {
|
||||
return [...failedMessageIds, clientMessageId];
|
||||
});
|
||||
const error = getErrorFromAcsResponseCode(
|
||||
'Error sending message, status code:',
|
||||
sendMessageResponse._response.status
|
||||
);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setFailedMessageIds((failedMessageIds) => {
|
||||
return [...failedMessageIds, clientMessageId];
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const processSendMessage = async (
|
||||
displayName: string,
|
||||
userId: string,
|
||||
chatThreadClient: ChatThreadClient,
|
||||
messageContent: string,
|
||||
threadId: string,
|
||||
setChatMessages: Dispatch<SetStateAction<ChatMessage[] | undefined>>,
|
||||
getMessage: (messageId: string) => Promise<ChatMessage | undefined>,
|
||||
setCoolPeriod: (coolPeriod: Date) => void,
|
||||
failedMessageIds: string[],
|
||||
setFailedMessageIds: Dispatch<SetStateAction<string[]>>
|
||||
): Promise<void> => {
|
||||
const clientMessageId = (Math.floor(Math.random() * MAXIMUM_INT64) + 1).toString(); //generate a random unsigned Int64 number
|
||||
const newMessage = {
|
||||
content: { message: messageContent },
|
||||
clientMessageId: clientMessageId,
|
||||
sender: { communicationUserId: userId },
|
||||
senderDisplayName: displayName,
|
||||
threadId: threadId,
|
||||
createdOn: new Date(),
|
||||
type: '',
|
||||
id: '',
|
||||
version: '',
|
||||
sequenceId: ''
|
||||
};
|
||||
setChatMessages((prevMessages) => {
|
||||
const messages: ChatMessage[] = prevMessages ? [...prevMessages] : [];
|
||||
messages.push(newMessage);
|
||||
return messages;
|
||||
});
|
||||
|
||||
await sendMessageWithClient(
|
||||
chatThreadClient,
|
||||
messageContent,
|
||||
displayName,
|
||||
clientMessageId,
|
||||
0,
|
||||
setChatMessages,
|
||||
getMessage,
|
||||
setCoolPeriod,
|
||||
failedMessageIds,
|
||||
setFailedMessageIds
|
||||
);
|
||||
};
|
||||
|
||||
export const useSendMessage = (displayName: string, userId: string): ((messageContent: string) => Promise<void>) => {
|
||||
const threadId = useThreadId();
|
||||
const setCoolPeriod = useSetCoolPeriod();
|
||||
|
||||
if (threadId === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ThreadId is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
|
||||
const chatThreadClient = useChatThreadClient();
|
||||
if (!chatThreadClient) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
|
||||
const setChatMessages: Dispatch<SetStateAction<ChatMessage[] | undefined>> = useSetChatMessages();
|
||||
const fetchMessage = useFetchMessage();
|
||||
|
||||
const setFailedMessageIds: Dispatch<SetStateAction<string[]>> = useSetFailedMessageIds();
|
||||
const failedMessageIds = useFailedMessageIds();
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (messageContent: string): Promise<void> => {
|
||||
await processSendMessage(
|
||||
displayName,
|
||||
userId,
|
||||
chatThreadClient,
|
||||
messageContent,
|
||||
threadId,
|
||||
setChatMessages,
|
||||
fetchMessage,
|
||||
setCoolPeriod,
|
||||
failedMessageIds,
|
||||
setFailedMessageIds
|
||||
);
|
||||
},
|
||||
[chatThreadClient, failedMessageIds, fetchMessage, setChatMessages, setCoolPeriod, setFailedMessageIds, threadId]
|
||||
);
|
||||
return sendMessage;
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { ThreadClientMock, createThreadClient } from '../mocks/ChatThreadClientMocks';
|
||||
|
||||
import { useSendReadReceipt } from './useSendReadReceipt';
|
||||
|
||||
let threadClientMock: ThreadClientMock;
|
||||
|
||||
const mockThreadClient = (): ThreadClientMock => {
|
||||
threadClientMock = createThreadClient();
|
||||
return threadClientMock;
|
||||
};
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useChatThreadClient: jest.fn().mockImplementation(
|
||||
(): ThreadClientMock => {
|
||||
return mockThreadClient();
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../providers/ErrorProvider', () => {
|
||||
return {
|
||||
useTriggerOnErrorCallback: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
describe('useSendReadReceipt test', () => {
|
||||
test('should be able to call sendReadReceipt inside useSendReadReceipt', async (): Promise<void> => {
|
||||
renderHook(() => {
|
||||
const sendReadReceipt = useSendReadReceipt();
|
||||
if (sendReadReceipt !== undefined) sendReadReceipt('mock message id');
|
||||
});
|
||||
expect(threadClientMock.sendReadReceipt).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
import { ChatThreadClient, SendReadReceiptRequest } from '@azure/communication-chat';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
CommunicationUiErrorCode,
|
||||
CommunicationUiError,
|
||||
CommunicationUiErrorSeverity
|
||||
} from '../types/CommunicationUiError';
|
||||
import { useChatThreadClient } from '../providers/ChatThreadProvider';
|
||||
import { getErrorFromAcsResponseCode } from '../utils/SDKUtils';
|
||||
|
||||
export const useSendReadReceipt = (): ((messageId: string) => Promise<void>) => {
|
||||
const chatThreadClient: ChatThreadClient | undefined = useChatThreadClient();
|
||||
const sendReadReceipt = useCallback(
|
||||
async (messageId: string): Promise<void> => {
|
||||
if (chatThreadClient === undefined) {
|
||||
// Read receipts aren't critical so we set the severity to IGNORE.
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.IGNORE
|
||||
});
|
||||
}
|
||||
const postReadReceiptRequest: SendReadReceiptRequest = {
|
||||
chatMessageId: messageId
|
||||
};
|
||||
let response;
|
||||
try {
|
||||
response = await chatThreadClient.sendReadReceipt(postReadReceiptRequest);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error sending read receipt',
|
||||
code: CommunicationUiErrorCode.SEND_READ_RECEIPT_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.IGNORE,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
const error = getErrorFromAcsResponseCode('Error sending read receipt, status code ', response._response.status);
|
||||
if (error) {
|
||||
// Read receipts aren't critical so we set the severity to IGNORE.
|
||||
error.severity = CommunicationUiErrorSeverity.IGNORE;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[chatThreadClient]
|
||||
);
|
||||
return sendReadReceipt;
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { ThreadClientMock, createThreadClient } from '../mocks/ChatThreadClientMocks';
|
||||
import { useSendTypingNotification } from './useSendTypingNotification';
|
||||
|
||||
let threadClientMock: ThreadClientMock;
|
||||
|
||||
const mockThreadClient = (): ThreadClientMock => {
|
||||
threadClientMock = createThreadClient();
|
||||
return threadClientMock;
|
||||
};
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useChatThreadClient: jest.fn().mockImplementation(
|
||||
(): ThreadClientMock => {
|
||||
return mockThreadClient();
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
describe('useSendTypingNotification test', () => {
|
||||
test('should be able to call sendTypingNotification inside useSendTypingNotification', async (): Promise<void> => {
|
||||
let sendTypingNotification: (() => Promise<boolean>) | undefined = undefined;
|
||||
renderHook(() => {
|
||||
sendTypingNotification = useSendTypingNotification();
|
||||
sendTypingNotification();
|
||||
});
|
||||
expect(threadClientMock.sendTypingNotification).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatThreadClient } from '@azure/communication-chat';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
CommunicationUiErrorCode,
|
||||
CommunicationUiError,
|
||||
CommunicationUiErrorSeverity
|
||||
} from '../types/CommunicationUiError';
|
||||
import { useChatThreadClient } from '../providers/ChatThreadProvider';
|
||||
|
||||
export const useSendTypingNotification = (): (() => Promise<boolean>) => {
|
||||
const chatThreadClient: ChatThreadClient | undefined = useChatThreadClient();
|
||||
const sendTypingNotification = useCallback(async (): Promise<boolean> => {
|
||||
if (chatThreadClient === undefined) {
|
||||
// Typing notifications aren't critical so we set the severity to IGNORE.
|
||||
throw new CommunicationUiError({
|
||||
message: 'ChatThreadClient is undefined',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.IGNORE
|
||||
});
|
||||
}
|
||||
try {
|
||||
return await chatThreadClient.sendTypingNotification();
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error sending typing notification',
|
||||
code: CommunicationUiErrorCode.SEND_TYPING_NOTIFICATION_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.IGNORE,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
}, [chatThreadClient]);
|
||||
return sendTypingNotification;
|
||||
};
|
|
@ -1,91 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatClientMock, createChatClient } from '../mocks/ChatClientMocks';
|
||||
import { ChatMessage } from '@azure/communication-chat';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { ChatMessageReceivedEvent } from '@azure/communication-signaling-2/types/src/events/chat';
|
||||
import { useSubscribeMessage } from './useSubscribeMessage';
|
||||
|
||||
let chatClientMock: ChatClientMock;
|
||||
|
||||
// Need an extra function start with mock to pass out the variable and lazily create mock
|
||||
// to byPass the restrcition of jest: `jest.mock()` is not allowed to reference any out-of-scope variables.
|
||||
const mockChatClient = (): ChatClientMock => {
|
||||
chatClientMock = createChatClient();
|
||||
return chatClientMock;
|
||||
};
|
||||
|
||||
jest.mock('../providers/ChatProviderHelper', () => {
|
||||
return {
|
||||
useChatClient: jest.fn().mockImplementation(
|
||||
(): ChatClientMock => {
|
||||
return mockChatClient();
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../providers/ChatProvider', () => {
|
||||
return {
|
||||
useUserId: () => 'dummyValueNotUsedByTest'
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../providers/ChatThreadProvider', () => {
|
||||
return {
|
||||
useSetChatMessages: () => () => {
|
||||
return;
|
||||
},
|
||||
useThreadId: () => 'ThreadId'
|
||||
};
|
||||
});
|
||||
|
||||
const mockMessage: ChatMessage = {
|
||||
content: { message: 'mockContent' },
|
||||
id: 'mockId',
|
||||
createdOn: new Date('2020-10-23T05:25:44.927Z'),
|
||||
sender: { communicationUserId: 'mockSenderCommunicationUserId' },
|
||||
senderDisplayName: 'mockSenderDisplayName',
|
||||
type: 'Text',
|
||||
version: 'mockVersion',
|
||||
sequenceId: ''
|
||||
};
|
||||
|
||||
const mockMessageEvent: ChatMessageReceivedEvent = {
|
||||
content: 'mockContent',
|
||||
createdOn: '2020-10-23T05:25:44.927Z',
|
||||
id: 'mockId',
|
||||
recipient: { communicationUserId: 'mockRecipientCommunicationUserId' },
|
||||
sender: { user: { communicationUserId: 'mockSenderCommunicationUserId' }, displayName: '' },
|
||||
threadId: 'mockThreadId',
|
||||
type: 'Text',
|
||||
version: 'mockVersion'
|
||||
};
|
||||
|
||||
describe('useSubscribeMessage tests', () => {
|
||||
afterEach(() => {
|
||||
chatClientMock.reset();
|
||||
});
|
||||
test('should be able to subscribe chatMessageReceived', async (): Promise<void> => {
|
||||
let messages: ChatMessage[] = [];
|
||||
const expectMessages = [mockMessage];
|
||||
const mockAddMessage = jest.fn((messageEvent: ChatMessageReceivedEvent) => {
|
||||
const { threadId: _threadId, recipient: _recipient, ...newMessage } = {
|
||||
...messageEvent,
|
||||
sender: { communicationUserId: messageEvent.sender.user.communicationUserId },
|
||||
content: { message: messageEvent.content },
|
||||
createdOn: new Date(messageEvent.createdOn),
|
||||
sequenceId: ''
|
||||
};
|
||||
messages = [...messages, newMessage];
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderHook(() => useSubscribeMessage(mockAddMessage));
|
||||
});
|
||||
await chatClientMock.triggerEvent('chatMessageReceived', mockMessageEvent);
|
||||
|
||||
expect(expectMessages).toMatchObject(messages);
|
||||
});
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatMessage } from '@azure/communication-chat';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { ChatMessageReceivedEvent } from '@azure/communication-signaling-2';
|
||||
import { useUserId } from '../providers/ChatProvider';
|
||||
import { useChatClient } from '../providers/ChatProviderHelper';
|
||||
import { useSetChatMessages, useThreadId } from '../providers/ChatThreadProvider';
|
||||
|
||||
const subscribedTheadIdSet = new Set<string>();
|
||||
|
||||
export const useSubscribeMessage = (addMessage?: (messageEvent: ChatMessageReceivedEvent) => void): void => {
|
||||
const chatClient = useChatClient();
|
||||
const setChatMessages = useSetChatMessages();
|
||||
const threadId = useThreadId();
|
||||
const userId = useUserId();
|
||||
|
||||
const defaultAddMessage = useCallback(
|
||||
(messageEvent: ChatMessageReceivedEvent) => {
|
||||
if (messageEvent.sender.user.communicationUserId !== userId) {
|
||||
// not user's own message
|
||||
setChatMessages((prevMessages) => {
|
||||
const messages: ChatMessage[] = prevMessages ? [...prevMessages] : [];
|
||||
const { threadId: _threadId, recipient: _recipient, ...newMessage } = {
|
||||
...messageEvent,
|
||||
sender: { communicationUserId: messageEvent.sender.user.communicationUserId },
|
||||
content: { message: messageEvent.content },
|
||||
createdOn: new Date(messageEvent.createdOn),
|
||||
sequenceId: ''
|
||||
};
|
||||
messages.push(newMessage);
|
||||
return messages;
|
||||
});
|
||||
}
|
||||
},
|
||||
[setChatMessages, userId]
|
||||
);
|
||||
|
||||
const onMessageReceived = useCallback(
|
||||
(event: ChatMessageReceivedEvent): void => {
|
||||
addMessage ? addMessage(event) : defaultAddMessage(event);
|
||||
},
|
||||
[addMessage, defaultAddMessage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
chatClient.on('chatMessageReceived', onMessageReceived);
|
||||
|
||||
if (!addMessage && threadId && !subscribedTheadIdSet.has(threadId)) {
|
||||
subscribedTheadIdSet.add(threadId);
|
||||
}
|
||||
|
||||
return () => {
|
||||
chatClient.off('chatMessageReceived', onMessageReceived);
|
||||
threadId && subscribedTheadIdSet.delete(threadId);
|
||||
};
|
||||
}, [chatClient, onMessageReceived, addMessage, threadId]);
|
||||
};
|
|
@ -1,65 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { ChatMessageReadReceipt } from '@azure/communication-chat';
|
||||
import { useChatClient } from '../providers/ChatProviderHelper';
|
||||
import { useFetchReadReceipts } from './useFetchReadReceipts';
|
||||
import { useSetReceipts, useThreadId } from '../providers/ChatThreadProvider';
|
||||
import { useTriggerOnErrorCallback } from '../providers/ErrorProvider';
|
||||
import { propagateError } from '../utils/SDKUtils';
|
||||
|
||||
const subscribedTheadIdSet = new Set<string>();
|
||||
|
||||
export const useSubscribeReadReceipt = (addReadReceipts?: (readReceipts: ChatMessageReadReceipt[]) => void): void => {
|
||||
const chatClient = useChatClient();
|
||||
const fetchReadReceipts = useFetchReadReceipts();
|
||||
const setReceipts = useSetReceipts();
|
||||
const threadId = useThreadId();
|
||||
const onErrorCallback = useTriggerOnErrorCallback();
|
||||
|
||||
const defaultAddReadReceipts = useCallback(
|
||||
(readReceipts: ChatMessageReadReceipt[]) => {
|
||||
setReceipts(readReceipts);
|
||||
},
|
||||
[setReceipts]
|
||||
);
|
||||
|
||||
const onReadReceiptReceived = useCallback(
|
||||
async (/*event: ReadReceiptReceivedEvent*/): Promise<void> => {
|
||||
// TODO: update to using readReceipt instead of readReceipts[]:
|
||||
// const _readReceipt: ReadReceipt = {
|
||||
// sender: event.sender,
|
||||
// chatMessageId: event.chatMessageId,
|
||||
// readOn: new Date(event.readOn)
|
||||
// };
|
||||
try {
|
||||
const readReceipts: ChatMessageReadReceipt[] = await fetchReadReceipts();
|
||||
addReadReceipts ? addReadReceipts(readReceipts) : defaultAddReadReceipts(readReceipts);
|
||||
} catch (error) {
|
||||
propagateError(error, onErrorCallback);
|
||||
}
|
||||
},
|
||||
[fetchReadReceipts, addReadReceipts, defaultAddReadReceipts, onErrorCallback]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const subscribeReadReceipt = async (): Promise<void> => {
|
||||
chatClient.on('readReceiptReceived', onReadReceiptReceived);
|
||||
};
|
||||
|
||||
if (addReadReceipts) {
|
||||
subscribeReadReceipt();
|
||||
} else if (threadId && !subscribedTheadIdSet.has(threadId)) {
|
||||
subscribeReadReceipt();
|
||||
subscribedTheadIdSet.add(threadId);
|
||||
}
|
||||
|
||||
return () => {
|
||||
chatClient.off('readReceiptReceived', onReadReceiptReceived);
|
||||
threadId && subscribedTheadIdSet.delete(threadId);
|
||||
};
|
||||
}, [addReadReceipts, chatClient, onReadReceiptReceived, threadId]);
|
||||
|
||||
return;
|
||||
};
|
|
@ -1,71 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { ChatClientMock, createChatClient } from '../mocks/ChatClientMocks';
|
||||
import {
|
||||
TypingNotification,
|
||||
TypingNotifications,
|
||||
useSubscribeTypingNotification
|
||||
} from './useSubscribeTypingNotification';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { TypingIndicatorReceivedEvent } from '@azure/communication-signaling-2';
|
||||
|
||||
let chatClientMock: ChatClientMock;
|
||||
|
||||
// Need an extra function start with mock to pass out the variable and lazily create mock
|
||||
// to byPass the restrcition of jest: `jest.mock()` is not allowed to reference any out-of-scope variables.
|
||||
const mockChatClient = (): ChatClientMock => {
|
||||
chatClientMock = createChatClient();
|
||||
return chatClientMock;
|
||||
};
|
||||
|
||||
jest.mock('../providers/ChatProviderHelper', () => {
|
||||
return {
|
||||
useChatClient: jest.fn().mockImplementation(
|
||||
(): ChatClientMock => {
|
||||
return mockChatClient();
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
const typingNotification: TypingNotification = {
|
||||
from: 'Test User',
|
||||
originalArrivalTime: 1577865600000, // number of Date '01-01-2020'
|
||||
recipientId: 'testId',
|
||||
threadId: 'testThreadId',
|
||||
version: '1'
|
||||
};
|
||||
|
||||
const typingIndicatorEvent: TypingIndicatorReceivedEvent = {
|
||||
version: typingNotification.version,
|
||||
receivedOn: new Date(1577865600000).toUTCString(),
|
||||
threadId: typingNotification.threadId,
|
||||
sender: {
|
||||
user: { communicationUserId: typingNotification.from },
|
||||
displayName: 'User1'
|
||||
},
|
||||
recipient: {
|
||||
communicationUserId: typingNotification.recipientId
|
||||
}
|
||||
};
|
||||
|
||||
describe('useSubscribeTypingNotification tests', () => {
|
||||
afterEach(() => {
|
||||
chatClientMock.reset();
|
||||
});
|
||||
test('should be able to subscribe typingIndicatorReceived', async (): Promise<void> => {
|
||||
let notifications: TypingNotifications = {};
|
||||
const expectNotifications = { [typingNotification.from]: typingNotification };
|
||||
const mockAddNotifications = jest.fn((notification: TypingNotification) => {
|
||||
notifications = { ...notifications, [notification.from]: notification };
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderHook(() => useSubscribeTypingNotification(mockAddNotifications));
|
||||
});
|
||||
await chatClientMock.triggerEvent('typingIndicatorReceived', typingIndicatorEvent);
|
||||
|
||||
expect(expectNotifications).toMatchObject(notifications);
|
||||
});
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { TypingIndicatorReceivedEvent } from '@azure/communication-signaling-2';
|
||||
import { useChatClient } from '../providers/ChatProviderHelper';
|
||||
|
||||
export type TypingNotification = {
|
||||
from: string;
|
||||
originalArrivalTime: number;
|
||||
recipientId: string;
|
||||
threadId: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type TypingNotifications = { [id: string]: TypingNotification };
|
||||
|
||||
export const useSubscribeTypingNotification = (
|
||||
addTypingNotifications: (notification: TypingNotification) => void
|
||||
): void => {
|
||||
const chatClient = useChatClient();
|
||||
|
||||
const onTypingIndicatorReceived = useCallback(
|
||||
(event: TypingIndicatorReceivedEvent): void => {
|
||||
const notification: TypingNotification = {
|
||||
from: event.sender.user.communicationUserId,
|
||||
originalArrivalTime: Date.parse(event.receivedOn),
|
||||
recipientId: event.recipient.communicationUserId,
|
||||
threadId: event.threadId,
|
||||
version: event.version
|
||||
};
|
||||
addTypingNotifications(notification);
|
||||
},
|
||||
[addTypingNotifications]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
chatClient.on('typingIndicatorReceived', onTypingIndicatorReceived);
|
||||
return () => {
|
||||
chatClient.off('typingIndicatorReceived', onTypingIndicatorReceived);
|
||||
};
|
||||
}, [chatClient, onTypingIndicatorReceived]);
|
||||
|
||||
return;
|
||||
};
|
|
@ -1,66 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
import { ChatParticipant } from '@azure/communication-chat';
|
||||
// © Microsoft Corporation. All rights reserved.
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { TypingNotification } from './useSubscribeTypingNotification';
|
||||
import { useTypingUsers } from './useTypingUsers';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
jest.mock('./useSubscribeTypingNotification', () => {
|
||||
let finished = false;
|
||||
return {
|
||||
useSubscribeTypingNotification: jest
|
||||
.fn()
|
||||
.mockImplementation((addTypingNotifications: (notification: TypingNotification) => void): void => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
const typingNotification: TypingNotification = {
|
||||
from: 'Test User',
|
||||
originalArrivalTime: new Date().getTime(), // number of Date '01-01-2020'
|
||||
recipientId: 'testId',
|
||||
threadId: 'testThreadId',
|
||||
version: '1'
|
||||
};
|
||||
addTypingNotifications(typingNotification);
|
||||
finished = true;
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const threadMembers: ChatParticipant[] = [{ user: { communicationUserId: 'Test User' } }];
|
||||
|
||||
describe('useTypingUsers tests', () => {
|
||||
test('should be able to generate typingUsers at the beginning and at 1 seconds', async (): Promise<void> => {
|
||||
const expectTypingUsers = [
|
||||
{
|
||||
user: { communicationUserId: 'Test User' }
|
||||
}
|
||||
];
|
||||
let typingUsers: ChatParticipant[] = [];
|
||||
|
||||
await act(async () => {
|
||||
renderHook(() => {
|
||||
typingUsers = useTypingUsers(threadMembers);
|
||||
});
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(typingUsers).toMatchObject(expectTypingUsers);
|
||||
});
|
||||
|
||||
test('should be able to clear typingUsers when expired', async (): Promise<void> => {
|
||||
const expectTypingUsers: ChatParticipant[] = [];
|
||||
let typingUsers: ChatParticipant[] = [];
|
||||
|
||||
await act(async () => {
|
||||
renderHook(() => {
|
||||
typingUsers = useTypingUsers(threadMembers);
|
||||
});
|
||||
jest.advanceTimersByTime(9000);
|
||||
});
|
||||
|
||||
expect(typingUsers).toMatchObject(expectTypingUsers);
|
||||
});
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
useSubscribeTypingNotification,
|
||||
TypingNotifications,
|
||||
TypingNotification
|
||||
} from './useSubscribeTypingNotification';
|
||||
import { MINIMUM_TYPING_INTERVAL_IN_MILLISECONDS } from '../constants';
|
||||
import { ChatParticipant } from '@azure/communication-chat';
|
||||
|
||||
const shouldDisplayTyping = (lastReceivedTypingEventDate: number): boolean => {
|
||||
const currentDate = new Date();
|
||||
const timeSinceLastTypingNotificationMs = currentDate.getTime() - lastReceivedTypingEventDate;
|
||||
return timeSinceLastTypingNotificationMs <= MINIMUM_TYPING_INTERVAL_IN_MILLISECONDS;
|
||||
};
|
||||
|
||||
export const compareUserArray = (array1: ChatParticipant[], array2: ChatParticipant[]): boolean => {
|
||||
const sortedArr2 = array2.sort();
|
||||
return (
|
||||
array1.length === array2.length &&
|
||||
array1.sort().every((value, index) => value.user.communicationUserId === sortedArr2[index].user.communicationUserId)
|
||||
);
|
||||
};
|
||||
|
||||
export const useTypingUsers = (threadMembers: ChatParticipant[]): ChatParticipant[] => {
|
||||
const [typingNotifications, setTypingNotifications] = useState<TypingNotifications>({});
|
||||
|
||||
const [typingUsers, setTypingUsers] = useState<ChatParticipant[]>([]);
|
||||
const [forceUpdateFlag, setForceUpdateFlag] = useState({});
|
||||
|
||||
const notificationRef = useRef(typingNotifications);
|
||||
const typingUsersRef = useRef(typingUsers);
|
||||
const updateTimerRef = useRef<number>();
|
||||
const threadMemberRef = useRef<ChatParticipant[]>([]);
|
||||
|
||||
const addTypingNotification = useCallback((notification: TypingNotification) => {
|
||||
setTypingNotifications((notifications: TypingNotifications) => ({
|
||||
...notifications,
|
||||
[notification.from]: notification
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useSubscribeTypingNotification(addTypingNotification);
|
||||
|
||||
const updateTypingUsers = useCallback(async () => {
|
||||
const currentTypingUsers: ChatParticipant[] = [];
|
||||
|
||||
for (const id in notificationRef.current) {
|
||||
const typingNotification = notificationRef.current[id];
|
||||
if (!typingNotification.originalArrivalTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldDisplayTyping(typingNotification.originalArrivalTime)) {
|
||||
const threadMember = threadMemberRef.current.find(
|
||||
(threadMember) => threadMember.user.communicationUserId === id
|
||||
);
|
||||
if (threadMember) {
|
||||
currentTypingUsers.push(threadMember);
|
||||
}
|
||||
} else {
|
||||
setTypingNotifications((notifications: TypingNotifications) => {
|
||||
const { [id]: _, ...newNotifications } = notifications;
|
||||
return newNotifications;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTypingUsers.length === 0) {
|
||||
if (typingUsersRef.current.length !== 0) {
|
||||
setTypingUsers([]);
|
||||
}
|
||||
|
||||
// If there are no longer any typing users, clear the timer and update state
|
||||
if (updateTimerRef.current) {
|
||||
clearInterval(updateTimerRef.current);
|
||||
updateTimerRef.current = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTypingUsers.length !== 0 && !compareUserArray(typingUsersRef.current ?? [], currentTypingUsers)) {
|
||||
setTypingUsers(currentTypingUsers);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
notificationRef.current = typingNotifications;
|
||||
typingUsersRef.current = typingUsers;
|
||||
threadMemberRef.current = threadMembers;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// This will ensure a render and run updateTypingUsers run at least every 500ms
|
||||
if (!updateTimerRef.current) {
|
||||
updateTimerRef.current = window.setInterval(() => setForceUpdateFlag({ value: updateTimerRef.current }), 500);
|
||||
}
|
||||
|
||||
updateTypingUsers();
|
||||
}, [typingNotifications, forceUpdateFlag, updateTypingUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (updateTimerRef.current) {
|
||||
clearInterval(updateTimerRef.current);
|
||||
updateTimerRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return typingUsers;
|
||||
};
|
|
@ -1,64 +0,0 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
CommunicationUiErrorCode,
|
||||
CommunicationUiError,
|
||||
CommunicationUiErrorSeverity
|
||||
} from '../types/CommunicationUiError';
|
||||
import { getErrorFromAcsResponseCode } from '../utils/SDKUtils';
|
||||
import { OK } from '../constants';
|
||||
import { useChatClient } from '../providers/ChatProviderHelper';
|
||||
import { useChatThreadClient, useSetThread, useThreadId } from '../providers/ChatThreadProvider';
|
||||
|
||||
export const useUpdateThreadTopicName = (): ((topicName: string) => Promise<boolean>) => {
|
||||
const threadId = useThreadId();
|
||||
const chatClient = useChatClient();
|
||||
const threadClient = useChatThreadClient();
|
||||
const setThread = useSetThread();
|
||||
const useUpdateThreadTopicNameInternal = useCallback(
|
||||
async (topicName: string): Promise<boolean> => {
|
||||
if (threadClient === undefined) {
|
||||
console.error('threadClient is undefined');
|
||||
return false;
|
||||
}
|
||||
if (threadId === undefined) {
|
||||
console.log('threadId is undefined');
|
||||
return false;
|
||||
}
|
||||
const updateThreadRequest = {
|
||||
topic: topicName
|
||||
};
|
||||
let res;
|
||||
try {
|
||||
res = await threadClient.updateThread(updateThreadRequest);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error updating thread',
|
||||
code: CommunicationUiErrorCode.UPDATE_THREAD_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.WARNING,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
if (res._response.status === OK) {
|
||||
try {
|
||||
const thread = await chatClient.getChatThread(threadId);
|
||||
setThread(thread);
|
||||
} catch (error) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'Error getting thread',
|
||||
code: CommunicationUiErrorCode.GET_THREAD_ERROR,
|
||||
severity: CommunicationUiErrorSeverity.WARNING
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const error = getErrorFromAcsResponseCode('Error updating thread, status code: ', res._response.status);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[chatClient, setThread, threadClient, threadId]
|
||||
);
|
||||
return useUpdateThreadTopicNameInternal;
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChatClient } from '@azure/communication-chat';
|
||||
import { chatClientDeclaratify } from '@azure/acs-chat-declarative';
|
||||
import { ChatThreadProvider } from './ChatThreadProvider';
|
||||
|
@ -43,14 +42,12 @@ type ChatProviderProps = {
|
|||
* @param props
|
||||
*/
|
||||
const ChatProviderBase = (props: ChatProviderProps & ErrorHandlingProps): JSX.Element => {
|
||||
const { token } = props;
|
||||
const { token, displayName } = props;
|
||||
const idFromToken = getIdFromToken(token);
|
||||
const [userId, setUserId] = useState<string>(idFromToken);
|
||||
const [displayName, setDisplayName] = useState<string>(props.displayName);
|
||||
const [chatClient, setChatClient] = useState<ChatClient>(
|
||||
chatClientDeclaratify(
|
||||
new ChatClient(props.endpointUrl, createAzureCommunicationUserCredentialBeta(token, props.refreshTokenCallback)),
|
||||
{ userId, displayName }
|
||||
{ userId: idFromToken, displayName }
|
||||
)
|
||||
);
|
||||
const [chatProviderState, setChatProviderState] = useState<number>(CHATPROVIDER_LOADING_STATE);
|
||||
|
@ -80,11 +77,7 @@ const ChatProviderBase = (props: ChatProviderProps & ErrorHandlingProps): JSX.El
|
|||
|
||||
contextState = {
|
||||
chatClient,
|
||||
setChatClient,
|
||||
userId,
|
||||
setUserId,
|
||||
displayName,
|
||||
setDisplayName
|
||||
setChatClient
|
||||
};
|
||||
|
||||
// We wait until realtime notifications are set up to avoid any potential bugs where code is relying on notifications
|
||||
|
@ -106,36 +99,3 @@ const ChatProviderBase = (props: ChatProviderProps & ErrorHandlingProps): JSX.El
|
|||
|
||||
export const ChatProvider = (props: ChatProviderProps & ErrorHandlingProps): JSX.Element =>
|
||||
WithErrorHandling(ChatProviderBase, props);
|
||||
|
||||
export const useSetChatClient = (): ((chatClient: ChatClient) => void) => {
|
||||
const chatContext = useContext<ChatContextType | undefined>(ChatContext);
|
||||
if (chatContext === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'UseSetChatClient invoked when ChatContext not initialized',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
return chatContext.setChatClient;
|
||||
};
|
||||
|
||||
export const useUserId = (): string => {
|
||||
const chatContext = useContext<ChatContextType | undefined>(ChatContext);
|
||||
if (chatContext === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'UseUserId invoked when ChatContext not initialized',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
return chatContext.userId;
|
||||
};
|
||||
|
||||
export const useDisplayName = (): string => {
|
||||
const chatContext = useContext<ChatContextType | undefined>(ChatContext);
|
||||
if (chatContext === undefined) {
|
||||
throw new CommunicationUiError({
|
||||
message: 'UseDisplayName invoked when ChatContext not initialized',
|
||||
code: CommunicationUiErrorCode.CONFIGURATION_ERROR
|
||||
});
|
||||
}
|
||||
return chatContext.displayName;
|
||||
};
|
||||
|
|
|
@ -19,10 +19,6 @@ export const ChatContext = createContext<ChatContextType | undefined>(undefined)
|
|||
export type ChatContextType = {
|
||||
chatClient?: ChatClient;
|
||||
setChatClient: Dispatch<SetStateAction<ChatClient>>;
|
||||
userId: string;
|
||||
setUserId: Dispatch<SetStateAction<string>>;
|
||||
displayName: string;
|
||||
setDisplayName: Dispatch<SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const useChatClient = (): ChatClient => {
|
||||
|
|
|
@ -1,44 +1,22 @@
|
|||
// © Microsoft Corporation. All rights reserved.
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
ChatThread,
|
||||
ChatThreadClient,
|
||||
ChatParticipant,
|
||||
ChatMessageReadReceipt
|
||||
} from '@azure/communication-chat';
|
||||
import { ChatThreadClient } from '@azure/communication-chat';
|
||||
import { Spinner } from '@fluentui/react';
|
||||
import { WithErrorHandling } from '../utils/WithErrorHandling';
|
||||
import React, { Dispatch, SetStateAction, createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useChatClient } from './ChatProviderHelper';
|
||||
import { ErrorHandlingProps } from './ErrorProvider';
|
||||
import {
|
||||
CommunicationUiErrorCode,
|
||||
CommunicationUiErrorFromError,
|
||||
CommunicationUiError
|
||||
} from '../types/CommunicationUiError';
|
||||
import { useChatClient } from './ChatProviderHelper';
|
||||
|
||||
export type ThreadProviderContextType = {
|
||||
chatThreadClient?: ChatThreadClient;
|
||||
setChatThreadClient: Dispatch<SetStateAction<ChatThreadClient | undefined>>;
|
||||
chatMessages?: ChatMessage[];
|
||||
setChatMessages: Dispatch<SetStateAction<ChatMessage[] | undefined>>;
|
||||
threadId: string;
|
||||
setThreadId: Dispatch<SetStateAction<string>>;
|
||||
thread: ChatThread | undefined;
|
||||
setThread: Dispatch<SetStateAction<ChatThread | undefined>>;
|
||||
receipts: ChatMessageReadReceipt[] | undefined;
|
||||
setReceipts: Dispatch<SetStateAction<ChatMessageReadReceipt[] | undefined>>;
|
||||
threadMembers: ChatParticipant[];
|
||||
setThreadMembers: Dispatch<SetStateAction<ChatParticipant[]>>;
|
||||
coolPeriod: Date | undefined;
|
||||
setCoolPeriod: Dispatch<SetStateAction<Date | undefined>>;
|
||||
getThreadMembersError: boolean | undefined;
|
||||
setGetThreadMembersError: Dispatch<SetStateAction<boolean | undefined>>;
|
||||
updateThreadMembersError: boolean | undefined;
|
||||
setUpdateThreadMembersError: Dispatch<SetStateAction<boolean | undefined>>;
|
||||
failedMessageIds: string[];
|
||||
setFailedMessageIds: Dispatch<SetStateAction<string[]>>;
|
||||
};
|
||||
|
||||
let contextState: ThreadProviderContextType;
|
||||
|
@ -59,38 +37,14 @@ type ChatThreadProviderProps = {
|
|||
|
||||
const ChatThreadProviderBase = (props: ChatThreadProviderProps & ErrorHandlingProps): JSX.Element => {
|
||||
const [chatThreadClient, setChatThreadClient] = useState<ChatThreadClient | undefined>();
|
||||
const [chatMessages, setChatMessages] = useState<ChatMessage[] | undefined>(undefined);
|
||||
const [threadId, setThreadId] = useState<string>(props.threadId);
|
||||
const [thread, setThread] = useState<ChatThread | undefined>(undefined);
|
||||
const [receipts, setReceipts] = useState<ChatMessageReadReceipt[] | undefined>(undefined);
|
||||
const [threadMembers, setThreadMembers] = useState<ChatParticipant[]>([]);
|
||||
const [getThreadMembersError, setGetThreadMembersError] = useState<boolean | undefined>(undefined);
|
||||
const [updateThreadMembersError, setUpdateThreadMembersError] = useState<boolean | undefined>(undefined);
|
||||
const [coolPeriod, setCoolPeriod] = useState<Date>();
|
||||
const [failedMessageIds, setFailedMessageIds] = useState<string[]>([]);
|
||||
const [chatThreadProviderState, setChatThreadProviderState] = useState<number>(CHATTHREADPROVIDER_LOADING_STATE);
|
||||
|
||||
contextState = {
|
||||
chatThreadClient,
|
||||
setChatThreadClient,
|
||||
receipts,
|
||||
setReceipts,
|
||||
threadId,
|
||||
setThreadId,
|
||||
thread,
|
||||
setThread,
|
||||
chatMessages,
|
||||
setChatMessages,
|
||||
threadMembers,
|
||||
setThreadMembers,
|
||||
coolPeriod,
|
||||
setCoolPeriod,
|
||||
getThreadMembersError,
|
||||
setGetThreadMembersError,
|
||||
updateThreadMembersError,
|
||||
setUpdateThreadMembersError,
|
||||
failedMessageIds,
|
||||
setFailedMessageIds
|
||||
setThreadId
|
||||
};
|
||||
|
||||
const chatClient = useChatClient();
|
||||
|
@ -151,21 +105,6 @@ export const useChatThreadClient = (): ChatThreadClient | undefined => {
|
|||
return threadContext.chatThreadClient;
|
||||
};
|
||||
|
||||
export const useSetChatThreadClient = (): ((threadClient: ChatThreadClient) => void) => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setChatThreadClient;
|
||||
};
|
||||
|
||||
export const useChatMessages = (): ChatMessage[] | undefined => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.chatMessages;
|
||||
};
|
||||
|
||||
export const useSetChatMessages = (): Dispatch<SetStateAction<ChatMessage[] | undefined>> => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setChatMessages;
|
||||
};
|
||||
|
||||
export const useThreadId = (): string => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.threadId;
|
||||
|
@ -175,73 +114,3 @@ export const useSetThreadId = (): ((threadId: string) => void) => {
|
|||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setThreadId;
|
||||
};
|
||||
|
||||
export const useThread = (): ChatThread | undefined => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.thread;
|
||||
};
|
||||
|
||||
export const useSetThread = (): ((thread: ChatThread) => void) => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setThread;
|
||||
};
|
||||
|
||||
export const useReceipts = (): ChatMessageReadReceipt[] | undefined => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.receipts;
|
||||
};
|
||||
|
||||
export const useSetReceipts = (): ((receipts: ChatMessageReadReceipt[]) => void) => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setReceipts;
|
||||
};
|
||||
|
||||
export const useThreadMembers = (): ChatParticipant[] => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.threadMembers;
|
||||
};
|
||||
|
||||
export const useSetThreadMembers = (): ((threadMembers: ChatParticipant[]) => void) => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setThreadMembers;
|
||||
};
|
||||
|
||||
export const useCoolPeriod = (): Date | undefined => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.coolPeriod;
|
||||
};
|
||||
|
||||
export const useSetCoolPeriod = (): ((coolPeriod: Date | undefined) => void) => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setCoolPeriod;
|
||||
};
|
||||
|
||||
export const useGetThreadMembersError = (): boolean | undefined => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.getThreadMembersError;
|
||||
};
|
||||
|
||||
export const useSetGetThreadMembersError = (): ((getThreadMembersError: boolean) => void) => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setGetThreadMembersError;
|
||||
};
|
||||
|
||||
export const useGetUpdateThreadMembersError = (): boolean | undefined => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.updateThreadMembersError;
|
||||
};
|
||||
|
||||
export const useSetUpdateThreadMembersError = (): ((updateThreadMembersError?: boolean) => void) => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setUpdateThreadMembersError;
|
||||
};
|
||||
|
||||
export const useFailedMessageIds = (): string[] => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.failedMessageIds;
|
||||
};
|
||||
|
||||
export const useSetFailedMessageIds = (): Dispatch<SetStateAction<string[]>> => {
|
||||
const threadContext = useValidateAndGetThreadContext();
|
||||
return threadContext.setFailedMessageIds;
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче