зеркало из
1
0
Форкнуть 0

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:
Eason 2021-04-27 14:17:55 -07:00 коммит произвёл GitHub
Родитель 3a517bbf16
Коммит edb4828cac
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
37 изменённых файлов: 97 добавлений и 2249 удалений

Просмотреть файл

@ -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;
};