diff --git a/change-beta/@azure-communication-react-0bde713d-dea9-4c4f-818d-5bcde6286354.json b/change-beta/@azure-communication-react-0bde713d-dea9-4c4f-818d-5bcde6286354.json new file mode 100644 index 00000000000..1a24a737af7 --- /dev/null +++ b/change-beta/@azure-communication-react-0bde713d-dea9-4c4f-818d-5bcde6286354.json @@ -0,0 +1,9 @@ +{ + "type": "patch", + "area": "fix", + "workstream": "", + "comment": "Update the unread messages count on the CallWithChatComposite when a message is deleted.", + "packageName": "@azure/communication-react", + "email": "2684369+JamesBurnside@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change-beta/@azure-communication-react-9086d50f-1734-491f-b814-ea14a009446e.json b/change-beta/@azure-communication-react-9086d50f-1734-491f-b814-ea14a009446e.json new file mode 100644 index 00000000000..b980d5d4058 --- /dev/null +++ b/change-beta/@azure-communication-react-9086d50f-1734-491f-b814-ea14a009446e.json @@ -0,0 +1,9 @@ +{ + "type": "patch", + "area": "improvement", + "workstream": "", + "comment": "Add messageEdited and messageDeleted events to the ChatAdapter", + "packageName": "@azure/communication-react", + "email": "2684369+JamesBurnside@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-react-0bde713d-dea9-4c4f-818d-5bcde6286354.json b/change/@azure-communication-react-0bde713d-dea9-4c4f-818d-5bcde6286354.json new file mode 100644 index 00000000000..1a24a737af7 --- /dev/null +++ b/change/@azure-communication-react-0bde713d-dea9-4c4f-818d-5bcde6286354.json @@ -0,0 +1,9 @@ +{ + "type": "patch", + "area": "fix", + "workstream": "", + "comment": "Update the unread messages count on the CallWithChatComposite when a message is deleted.", + "packageName": "@azure/communication-react", + "email": "2684369+JamesBurnside@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-react-9086d50f-1734-491f-b814-ea14a009446e.json b/change/@azure-communication-react-9086d50f-1734-491f-b814-ea14a009446e.json new file mode 100644 index 00000000000..b980d5d4058 --- /dev/null +++ b/change/@azure-communication-react-9086d50f-1734-491f-b814-ea14a009446e.json @@ -0,0 +1,9 @@ +{ + "type": "patch", + "area": "improvement", + "workstream": "", + "comment": "Add messageEdited and messageDeleted events to the ChatAdapter", + "packageName": "@azure/communication-react", + "email": "2684369+JamesBurnside@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index ae0d4ec4465..5ef3f930160 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -1071,6 +1071,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) off(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + off(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + off(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) off(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) off(event: 'messageRead', listener: MessageReadListener): void; @@ -1115,6 +1119,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) on(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + on(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + on(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) on(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) on(event: 'messageRead', listener: MessageReadListener): void; @@ -1323,7 +1331,7 @@ export interface CallWithChatControlOptions extends CommonCallControlOptions { } // @public -export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; +export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageEdited' | 'messageDeleted' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; // @beta export const CameraAndMicrophoneSitePermissions: (props: CameraAndMicrophoneSitePermissionsProps) => JSX.Element; @@ -1543,6 +1551,8 @@ export type ChatAdapterState = ChatAdapterUiState & ChatCompositeClientState; // @public export interface ChatAdapterSubscribers { off(event: 'messageReceived', listener: MessageReceivedListener): void; + off(event: 'messageEdited', listener: MessageEditedListener): void; + off(event: 'messageDeleted', listener: MessageDeletedListener): void; off(event: 'messageSent', listener: MessageSentListener): void; off(event: 'messageRead', listener: MessageReadListener): void; off(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -1550,6 +1560,8 @@ export interface ChatAdapterSubscribers { off(event: 'topicChanged', listener: TopicChangedListener): void; off(event: 'error', listener: (e: AdapterError) => void): void; on(event: 'messageReceived', listener: MessageReceivedListener): void; + on(event: 'messageEdited', listener: MessageEditedListener): void; + on(event: 'messageDeleted', listener: MessageDeletedListener): void; on(event: 'messageSent', listener: MessageSentListener): void; on(event: 'messageRead', listener: MessageReadListener): void; on(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -3123,6 +3135,12 @@ export interface MessageCommon { // @public export type MessageContentType = 'text' | 'html' | 'richtext/html' | 'unknown'; +// @public +export type MessageDeletedListener = MessageReceivedListener; + +// @public +export type MessageEditedListener = MessageReceivedListener; + // @public export type MessageProps = { message: Message; diff --git a/packages/communication-react/review/stable/communication-react.api.md b/packages/communication-react/review/stable/communication-react.api.md index 47cc1c7841a..943b62cd98d 100644 --- a/packages/communication-react/review/stable/communication-react.api.md +++ b/packages/communication-react/review/stable/communication-react.api.md @@ -805,6 +805,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) off(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + off(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + off(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) off(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) off(event: 'messageRead', listener: MessageReadListener): void; @@ -849,6 +853,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) on(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + on(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + on(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) on(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) on(event: 'messageRead', listener: MessageReadListener): void; @@ -1012,7 +1020,7 @@ export interface CallWithChatControlOptions extends CommonCallControlOptions { } // @public -export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; +export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageEdited' | 'messageDeleted' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; // @public export const CameraButton: (props: CameraButtonProps) => JSX.Element; @@ -1206,6 +1214,8 @@ export type ChatAdapterState = ChatAdapterUiState & ChatCompositeClientState; // @public export interface ChatAdapterSubscribers { off(event: 'messageReceived', listener: MessageReceivedListener): void; + off(event: 'messageEdited', listener: MessageEditedListener): void; + off(event: 'messageDeleted', listener: MessageDeletedListener): void; off(event: 'messageSent', listener: MessageSentListener): void; off(event: 'messageRead', listener: MessageReadListener): void; off(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -1213,6 +1223,8 @@ export interface ChatAdapterSubscribers { off(event: 'topicChanged', listener: TopicChangedListener): void; off(event: 'error', listener: (e: AdapterError) => void): void; on(event: 'messageReceived', listener: MessageReceivedListener): void; + on(event: 'messageEdited', listener: MessageEditedListener): void; + on(event: 'messageDeleted', listener: MessageDeletedListener): void; on(event: 'messageSent', listener: MessageSentListener): void; on(event: 'messageRead', listener: MessageReadListener): void; on(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -2460,6 +2472,12 @@ export interface MessageCommon { // @public export type MessageContentType = 'text' | 'html' | 'richtext/html' | 'unknown'; +// @public +export type MessageDeletedListener = MessageReceivedListener; + +// @public +export type MessageEditedListener = MessageReceivedListener; + // @public export type MessageProps = { message: Message; diff --git a/packages/react-composites/review/beta/react-composites.api.md b/packages/react-composites/review/beta/react-composites.api.md index e24a1f9ae50..2fd138ed9b2 100644 --- a/packages/react-composites/review/beta/react-composites.api.md +++ b/packages/react-composites/review/beta/react-composites.api.md @@ -772,6 +772,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) off(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + off(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + off(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) off(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) off(event: 'messageRead', listener: MessageReadListener): void; @@ -816,6 +820,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) on(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + on(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + on(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) on(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) on(event: 'messageRead', listener: MessageReadListener): void; @@ -1024,7 +1032,7 @@ export interface CallWithChatControlOptions extends CommonCallControlOptions { } // @public -export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; +export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageEdited' | 'messageDeleted' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; // @public export type CapabilitiesChangedListener = (data: CapabilitiesChangeInfo) => void; @@ -1059,6 +1067,8 @@ export type ChatAdapterState = ChatAdapterUiState & ChatCompositeClientState; // @public export interface ChatAdapterSubscribers { off(event: 'messageReceived', listener: MessageReceivedListener): void; + off(event: 'messageEdited', listener: MessageEditedListener): void; + off(event: 'messageDeleted', listener: MessageDeletedListener): void; off(event: 'messageSent', listener: MessageSentListener): void; off(event: 'messageRead', listener: MessageReadListener): void; off(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -1066,6 +1076,8 @@ export interface ChatAdapterSubscribers { off(event: 'topicChanged', listener: TopicChangedListener): void; off(event: 'error', listener: (e: AdapterError) => void): void; on(event: 'messageReceived', listener: MessageReceivedListener): void; + on(event: 'messageEdited', listener: MessageEditedListener): void; + on(event: 'messageDeleted', listener: MessageDeletedListener): void; on(event: 'messageSent', listener: MessageSentListener): void; on(event: 'messageRead', listener: MessageReadListener): void; on(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -1660,6 +1672,12 @@ export type MediaDiagnosticChangedEvent = MediaDiagnosticChangedEventArgs & { type: 'media'; }; +// @public +export type MessageDeletedListener = MessageReceivedListener; + +// @public +export type MessageEditedListener = MessageReceivedListener; + // @public export type MessageReadListener = (event: { message: ChatMessage; diff --git a/packages/react-composites/review/stable/react-composites.api.md b/packages/react-composites/review/stable/react-composites.api.md index 46b2a5d5d39..65453c55a75 100644 --- a/packages/react-composites/review/stable/react-composites.api.md +++ b/packages/react-composites/review/stable/react-composites.api.md @@ -605,6 +605,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) off(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + off(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + off(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) off(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) off(event: 'messageRead', listener: MessageReadListener): void; @@ -649,6 +653,10 @@ export interface CallWithChatAdapterSubscriptions { // (undocumented) on(event: 'messageReceived', listener: MessageReceivedListener): void; // (undocumented) + on(event: 'messageEdited', listener: MessageEditedListener): void; + // (undocumented) + on(event: 'messageDeleted', listener: MessageDeletedListener): void; + // (undocumented) on(event: 'messageSent', listener: MessageSentListener): void; // (undocumented) on(event: 'messageRead', listener: MessageReadListener): void; @@ -812,7 +820,7 @@ export interface CallWithChatControlOptions extends CommonCallControlOptions { } // @public -export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; +export type CallWithChatEvent = 'callError' | 'chatError' | 'callEnded' | 'isMutedChanged' | 'callIdChanged' | 'isLocalScreenSharingActiveChanged' | 'displayNameChanged' | 'isSpeakingChanged' | 'callParticipantsJoined' | 'callParticipantsLeft' | 'selectedMicrophoneChanged' | 'selectedSpeakerChanged' | /* @conditional-compile-remove(close-captions) */ 'isCaptionsActiveChanged' | /* @conditional-compile-remove(close-captions) */ 'captionsReceived' | /* @conditional-compile-remove(close-captions) */ 'isCaptionLanguageChanged' | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' | 'messageEdited' | 'messageDeleted' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' | 'chatParticipantsRemoved'; // @public export type CapabilitiesChangedListener = (data: CapabilitiesChangeInfo) => void; @@ -847,6 +855,8 @@ export type ChatAdapterState = ChatAdapterUiState & ChatCompositeClientState; // @public export interface ChatAdapterSubscribers { off(event: 'messageReceived', listener: MessageReceivedListener): void; + off(event: 'messageEdited', listener: MessageEditedListener): void; + off(event: 'messageDeleted', listener: MessageDeletedListener): void; off(event: 'messageSent', listener: MessageSentListener): void; off(event: 'messageRead', listener: MessageReadListener): void; off(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -854,6 +864,8 @@ export interface ChatAdapterSubscribers { off(event: 'topicChanged', listener: TopicChangedListener): void; off(event: 'error', listener: (e: AdapterError) => void): void; on(event: 'messageReceived', listener: MessageReceivedListener): void; + on(event: 'messageEdited', listener: MessageEditedListener): void; + on(event: 'messageDeleted', listener: MessageDeletedListener): void; on(event: 'messageSent', listener: MessageSentListener): void; on(event: 'messageRead', listener: MessageReadListener): void; on(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -1086,7 +1098,7 @@ export const createAzureCommunicationCallWithChatAdapterFromClients: ({ callClie export const createAzureCommunicationChatAdapter: ({ endpoint: endpointUrl, userId, displayName, credential, threadId }: AzureCommunicationChatAdapterArgs) => Promise; // @public -export function createAzureCommunicationChatAdapterFromClient(chatClient: StatefulChatClient, chatThreadClient: ChatThreadClient, options?: { +export function createAzureCommunicationChatAdapterFromClient(chatClient: StatefulChatClient, chatThreadClient: ChatThreadClient, options?: { credential?: CommunicationTokenCredential; }): Promise; @@ -1330,6 +1342,12 @@ export type MediaDiagnosticChangedEvent = MediaDiagnosticChangedEventArgs & { type: 'media'; }; +// @public +export type MessageDeletedListener = MessageReceivedListener; + +// @public +export type MessageEditedListener = MessageReceivedListener; + // @public export type MessageReadListener = (event: { message: ChatMessage; diff --git a/packages/react-composites/src/composites/CallWithChatComposite/ChatButton/useUnreadMessagesTracker.ts b/packages/react-composites/src/composites/CallWithChatComposite/ChatButton/useUnreadMessagesTracker.ts index fb192cadcee..37edcaacfe0 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/ChatButton/useUnreadMessagesTracker.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/ChatButton/useUnreadMessagesTracker.ts @@ -10,26 +10,48 @@ import { ChatMessage } from '@azure/communication-chat'; * @private */ export const useUnreadMessagesTracker = (chatAdapter: ChatAdapter, isChatPaneVisible: boolean): number => { - const [unreadChatMessagesCount, setUnreadChatMessagesCount] = useState(0); + // Store messageIds of unread messages + const [unreadChatMessages, setUnreadChatMessages] = useState>(new Set()); useEffect(() => { + // Clear unread messages when chat pane is opened if (isChatPaneVisible) { - setUnreadChatMessagesCount(0); + setUnreadChatMessages(new Set()); return; } + + // Increment unread messages when a new message is received and the chat pane is closed const incrementUnreadChatMessagesCount = (event: { message: ChatMessage }): void => { if (!isChatPaneVisible && validNewChatMessage(event.message)) { - setUnreadChatMessagesCount(unreadChatMessagesCount + 1); + setUnreadChatMessages((prevUnreadChatMessages) => { + const newUnreadChatMessages = new Set(prevUnreadChatMessages); + newUnreadChatMessages.add(event.message.id); + return newUnreadChatMessages; + }); } }; + + // Decrement unread messages when a message is deleted and the chat pane is closed + const decrementUnreadChatMessagesCount = (event: { message: ChatMessage }): void => { + if (!isChatPaneVisible) { + setUnreadChatMessages((prevUnreadChatMessages) => { + const newUnreadChatMessages = new Set(prevUnreadChatMessages); + newUnreadChatMessages.delete(event.message.id); + return newUnreadChatMessages; + }); + } + }; + chatAdapter.on('messageReceived', incrementUnreadChatMessagesCount); + chatAdapter.on('messageDeleted', decrementUnreadChatMessagesCount); return () => { chatAdapter.off('messageReceived', incrementUnreadChatMessagesCount); + chatAdapter.off('messageDeleted', decrementUnreadChatMessagesCount); }; - }, [chatAdapter, setUnreadChatMessagesCount, isChatPaneVisible, unreadChatMessagesCount]); + }, [chatAdapter, setUnreadChatMessages, isChatPaneVisible]); - return unreadChatMessagesCount; + return unreadChatMessages.size; }; /** diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index 9bb24546f08..7fc601027c7 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -41,7 +41,9 @@ import { ChatAdapter, ChatAdapterState, ParticipantsRemovedListener, - ParticipantsAddedListener + ParticipantsAddedListener, + MessageEditedListener, + MessageDeletedListener } from '../../ChatComposite'; /* @conditional-compile-remove(file-sharing) */ import { FileUploadManager } from '../../ChatComposite'; @@ -550,6 +552,8 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte on(event: 'displayNameChanged', listener: DisplayNameChangedListener): void; on(event: 'isSpeakingChanged', listener: IsSpeakingChangedListener): void; on(event: 'messageReceived', listener: MessageReceivedListener): void; + on(event: 'messageEdited', listener: MessageEditedListener): void; + on(event: 'messageDeleted', listener: MessageDeletedListener): void; on(event: 'messageSent', listener: MessageReceivedListener): void; on(event: 'messageRead', listener: MessageReadListener): void; on(event: 'chatParticipantsAdded', listener: ParticipantsAddedListener): void; @@ -620,6 +624,12 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte case 'messageReceived': this.chatAdapter.on('messageReceived', listener); break; + case 'messageEdited': + this.chatAdapter.on('messageEdited', listener); + break; + case 'messageDeleted': + this.chatAdapter.on('messageDeleted', listener); + break; case 'messageSent': this.chatAdapter.on('messageSent', listener); break; @@ -656,6 +666,8 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte off(event: 'selectedMicrophoneChanged', listener: PropertyChangedEvent): void; off(event: 'selectedSpeakerChanged', listener: PropertyChangedEvent): void; off(event: 'messageReceived', listener: MessageReceivedListener): void; + off(event: 'messageEdited', listener: MessageEditedListener): void; + off(event: 'messageDeleted', listener: MessageDeletedListener): void; off(event: 'messageSent', listener: MessageReceivedListener): void; off(event: 'messageRead', listener: MessageReadListener): void; off(event: 'chatParticipantsAdded', listener: ParticipantsAddedListener): void; @@ -724,6 +736,12 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte case 'messageReceived': this.chatAdapter.off('messageReceived', listener); break; + case 'messageEdited': + this.chatAdapter.off('messageEdited', listener); + break; + case 'messageDeleted': + this.chatAdapter.off('messageDeleted', listener); + break; case 'messageSent': this.chatAdapter.off('messageSent', listener); break; diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index 951397aa190..82f98301098 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -15,6 +15,8 @@ import { CallEndedListener } from '../../CallComposite'; import { + MessageDeletedListener, + MessageEditedListener, MessageReadListener, MessageReceivedListener, MessageSentListener, @@ -522,6 +524,8 @@ export interface CallWithChatAdapterSubscriptions { // Chat subscriptions on(event: 'messageReceived', listener: MessageReceivedListener): void; + on(event: 'messageEdited', listener: MessageEditedListener): void; + on(event: 'messageDeleted', listener: MessageDeletedListener): void; on(event: 'messageSent', listener: MessageSentListener): void; on(event: 'messageRead', listener: MessageReadListener): void; on(event: 'chatParticipantsAdded', listener: ParticipantsAddedListener): void; @@ -529,6 +533,8 @@ export interface CallWithChatAdapterSubscriptions { on(event: 'chatError', listener: (e: AdapterError) => void): void; off(event: 'messageReceived', listener: MessageReceivedListener): void; + off(event: 'messageEdited', listener: MessageEditedListener): void; + off(event: 'messageDeleted', listener: MessageDeletedListener): void; off(event: 'messageSent', listener: MessageSentListener): void; off(event: 'messageRead', listener: MessageReadListener): void; off(event: 'chatParticipantsAdded', listener: ParticipantsAddedListener): void; @@ -571,6 +577,8 @@ export type CallWithChatEvent = | /* @conditional-compile-remove(close-captions) */ 'isSpokenLanguageChanged' | /* @conditional-compile-remove(capabilities) */ 'capabilitiesChanged' | 'messageReceived' + | 'messageEdited' + | 'messageDeleted' | 'messageSent' | 'messageRead' | 'chatParticipantsAdded' diff --git a/packages/react-composites/src/composites/ChatComposite/adapter/AzureCommunicationChatAdapter.ts b/packages/react-composites/src/composites/ChatComposite/adapter/AzureCommunicationChatAdapter.ts index 35f87f1d9df..2c10ade0d2b 100644 --- a/packages/react-composites/src/composites/ChatComposite/adapter/AzureCommunicationChatAdapter.ts +++ b/packages/react-composites/src/composites/ChatComposite/adapter/AzureCommunicationChatAdapter.ts @@ -11,6 +11,8 @@ import { ChatHandlers, createDefaultChatHandlers } from '@internal/chat-componen import { ChatMessage, ChatMessageType, ChatThreadClient, SendMessageOptions } from '@azure/communication-chat'; import { CommunicationTokenCredential, CommunicationUserIdentifier } from '@azure/communication-common'; import type { + ChatMessageDeletedEvent, + ChatMessageEditedEvent, ChatMessageReceivedEvent, ChatThreadPropertiesUpdatedEvent, ParticipantsAddedEvent, @@ -22,6 +24,8 @@ import EventEmitter from 'events'; import { ChatAdapter, ChatAdapterState, + MessageDeletedListener, + MessageEditedListener, MessageReadListener, MessageReceivedListener, ParticipantsAddedListener, @@ -388,6 +392,26 @@ export class AzureCommunicationChatAdapter implements ChatAdapter { } } + private messageEditedListener(event: ChatMessageEditedEvent): void { + const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId; + if (!isCurrentChatAdapterThread) { + return; + } + + const message = convertEventToChatMessage(event); + this.emitter.emit('messageEdited', { message }); + } + + private messageDeletedListener(event: ChatMessageDeletedEvent): void { + const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId; + if (!isCurrentChatAdapterThread) { + return; + } + + const message = convertEventToChatMessage(event); + this.emitter.emit('messageDeleted', { message }); + } + private messageReadListener({ chatMessageId, recipient }: ReadReceiptReceivedEvent): void { const message = this.getState().thread.chatMessages[chatMessageId]; if (message) { @@ -412,6 +436,8 @@ export class AzureCommunicationChatAdapter implements ChatAdapter { this.chatClient.on('participantsAdded', this.participantsAddedListener.bind(this)); this.chatClient.on('participantsRemoved', this.participantsRemovedListener.bind(this)); this.chatClient.on('chatMessageReceived', this.messageReceivedListener.bind(this)); + this.chatClient.on('chatMessageEdited', this.messageEditedListener.bind(this)); + this.chatClient.on('chatMessageDeleted', this.messageDeletedListener.bind(this)); this.chatClient.on('readReceiptReceived', this.messageReadListener.bind(this)); this.chatClient.on('participantsRemoved', this.participantsRemovedListener.bind(this)); } @@ -421,11 +447,15 @@ export class AzureCommunicationChatAdapter implements ChatAdapter { this.chatClient.off('participantsAdded', this.participantsAddedListener.bind(this)); this.chatClient.off('participantsRemoved', this.participantsRemovedListener.bind(this)); this.chatClient.off('chatMessageReceived', this.messageReceivedListener.bind(this)); + this.chatClient.off('chatMessageEdited', this.messageEditedListener.bind(this)); + this.chatClient.off('chatMessageDeleted', this.messageDeletedListener.bind(this)); this.chatClient.off('readReceiptReceived', this.messageReadListener.bind(this)); this.chatClient.off('participantsRemoved', this.participantsRemovedListener.bind(this)); } on(event: 'messageReceived', listener: MessageReceivedListener): void; + on(event: 'messageEdited', listener: MessageEditedListener): void; + on(event: 'messageDeleted', listener: MessageDeletedListener): void; on(event: 'messageSent', listener: MessageReceivedListener): void; on(event: 'messageRead', listener: MessageReadListener): void; on(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -439,6 +469,8 @@ export class AzureCommunicationChatAdapter implements ChatAdapter { } off(event: 'messageReceived', listener: MessageReceivedListener): void; + off(event: 'messageEdited', listener: MessageEditedListener): void; + off(event: 'messageDeleted', listener: MessageDeletedListener): void; off(event: 'messageSent', listener: MessageReceivedListener): void; off(event: 'messageRead', listener: MessageReadListener): void; off(event: 'participantsAdded', listener: ParticipantsAddedListener): void; @@ -463,19 +495,35 @@ export class AzureCommunicationChatAdapter implements ChatAdapter { } } -const convertEventToChatMessage = (event: ChatMessageReceivedEvent): ChatMessage => { +const convertEventToChatMessage = ( + event: ChatMessageReceivedEvent | ChatMessageEditedEvent | ChatMessageDeletedEvent +): ChatMessage => { return { id: event.id, version: event.version, - content: { message: event.message }, + content: isChatMessageDeletedEvent(event) ? undefined : { message: event.message }, type: convertEventType(event.type), sender: event.sender, senderDisplayName: event.senderDisplayName, sequenceId: '', - createdOn: new Date(event.createdOn) + createdOn: new Date(event.createdOn), + editedOn: isChatMessageEditedEvent(event) ? event.editedOn : undefined, + deletedOn: isChatMessageDeletedEvent(event) ? event.deletedOn : undefined }; }; +const isChatMessageEditedEvent = ( + event: ChatMessageReceivedEvent | ChatMessageEditedEvent | ChatMessageDeletedEvent +): event is ChatMessageEditedEvent => { + return event['editedOn'] !== undefined; +}; + +const isChatMessageDeletedEvent = ( + event: ChatMessageReceivedEvent | ChatMessageEditedEvent | ChatMessageDeletedEvent +): event is ChatMessageDeletedEvent => { + return event['deletedOn'] !== undefined; +}; + // only text/html message type will be received from event const convertEventType = (type: string): ChatMessageType => { const lowerCaseType = type.toLowerCase(); diff --git a/packages/react-composites/src/composites/ChatComposite/adapter/ChatAdapter.ts b/packages/react-composites/src/composites/ChatComposite/adapter/ChatAdapter.ts index 1ae8cd416f9..eb2ac459583 100644 --- a/packages/react-composites/src/composites/ChatComposite/adapter/ChatAdapter.ts +++ b/packages/react-composites/src/composites/ChatComposite/adapter/ChatAdapter.ts @@ -123,6 +123,14 @@ export interface ChatAdapterSubscribers { * Subscribe function for 'messageReceived' event. */ on(event: 'messageReceived', listener: MessageReceivedListener): void; + /** + * Subscribe function for 'messageEdited' event. + */ + on(event: 'messageEdited', listener: MessageEditedListener): void; + /** + * Subscribe function for 'messageDeleted' event. + */ + on(event: 'messageDeleted', listener: MessageDeletedListener): void; /** * Subscribe function for 'messageSent' event. */ @@ -152,6 +160,14 @@ export interface ChatAdapterSubscribers { * Unsubscribe function for 'messageReceived' event. */ off(event: 'messageReceived', listener: MessageReceivedListener): void; + /** + * Unsubscribe function for 'messageEdited' event. + */ + off(event: 'messageEdited', listener: MessageEditedListener): void; + /** + * Unsubscribe function for 'messageDeleted' event. + */ + off(event: 'messageDeleted', listener: MessageDeletedListener): void; /** * Unsubscribe function for 'messageSent' event. */ @@ -204,6 +220,20 @@ export type MessageReceivedListener = (event: { message: ChatMessage }) => void; */ export type MessageSentListener = MessageReceivedListener; +/** + * Callback for {@link ChatAdapterSubscribers} 'messageEdited' event. + * + * @public + */ +export type MessageEditedListener = MessageReceivedListener; + +/** + * Callback for {@link ChatAdapterSubscribers} 'messageDeleted' event. + * + * @public + */ +export type MessageDeletedListener = MessageReceivedListener; + /** * Callback for {@link ChatAdapterSubscribers} 'messageRead' event. * diff --git a/packages/react-composites/src/composites/ChatComposite/index.ts b/packages/react-composites/src/composites/ChatComposite/index.ts index 21723f86b20..a812d5a4bc7 100644 --- a/packages/react-composites/src/composites/ChatComposite/index.ts +++ b/packages/react-composites/src/composites/ChatComposite/index.ts @@ -22,6 +22,8 @@ export type { MessageReadListener, MessageReceivedListener, MessageSentListener, + MessageEditedListener, + MessageDeletedListener, ParticipantsAddedListener, ParticipantsRemovedListener, TopicChangedListener