Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Fix unread chat messages badge on the CallWithChatComposite being cleared when the call goes on hold",
"packageName": "@azure/communication-react",
"email": "2684369+JamesBurnside@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Fix unread chat messages badge on the CallWithChatComposite being cleared when the call goes on hold",
"packageName": "@azure/communication-react",
"email": "2684369+JamesBurnside@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { isDisabled } from '../CallComposite/utils';
import { CustomCallControlButtonCallback } from '../common/ControlBar/CustomButton';
import { SidePaneHeader } from '../common/SidePaneHeader';
import { _CallControlOptions } from '../CallComposite/types/CallControlOptions';
import { useUnreadMessagesTracker } from './ChatButton/useUnreadMessagesTracker';

/**
* Props required for the {@link CallWithChatComposite}
Expand Down Expand Up @@ -283,32 +284,36 @@ const CallWithChatScreen = (props: CallWithChatScreenProps): JSX.Element => {
[chatButtonDisabled, mobileView, toggleChat, showChatButton]
);

const unreadChatMessagesCount = useUnreadMessagesTracker(chatProps.adapter, isChatOpen);

const customChatButton: CustomCallControlButtonCallback = useCallback(
(args: CustomCallControlButtonCallbackArgs) => ({
placement: mobileView ? 'primary' : 'secondary',
onRenderButton: () => (
<ChatButtonWithUnreadMessagesBadge
chatAdapter={chatProps.adapter}
isChatPaneVisible={isChatOpen}
checked={isChatOpen}
showLabel={args.displayType !== 'compact'}
onClick={toggleChat}
disabled={chatButtonDisabled}
strings={chatButtonStrings}
styles={commonButtonStyles}
newMessageLabel={callWithChatStrings.chatButtonNewMessageNotificationLabel}
unreadChatMessagesCount={unreadChatMessagesCount}
// As chat is disabled when on hold, we don't want to show the unread badge when on hold
hideUnreadChatMessagesBadge={isOnHold}
/>
)
}),
[
callWithChatStrings.chatButtonNewMessageNotificationLabel,
chatButtonStrings,
chatProps.adapter,
commonButtonStyles,
isChatOpen,
chatButtonDisabled,
mobileView,
toggleChat
toggleChat,
unreadChatMessagesCount,
isOnHold
]
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { ChatMessage } from '@azure/communication-chat';
import { IStackStyles, Stack } from '@fluentui/react';
import { _formatString } from '@internal/acs-ui-common';
import { ControlBarButtonProps } from '@internal/react-components';
import React, { useCallback, useMemo, useState } from 'react';
import { useEffect } from 'react';
import { ChatAdapter } from '../../ChatComposite';
import React, { useCallback, useMemo } from 'react';
import { CallWithChatCompositeIcon } from '../../common/icons';
import { ChatButton } from './ChatButton';
import { useCallWithChatCompositeStrings } from '../hooks/useCallWithChatCompositeStrings';
Expand All @@ -17,27 +14,19 @@ import { NotificationIcon } from './NotificationIcon';
* @private
*/
export interface ChatButtonWithUnreadMessagesBadgeProps extends ControlBarButtonProps {
chatAdapter: ChatAdapter;
isChatPaneVisible: boolean;
newMessageLabel?: string;
unreadChatMessagesCount: number;
hideUnreadChatMessagesBadge?: boolean;
newMessageLabel: string;
}

/**
* Helper function to determine if the message in the event is a valid one from a user.
* Display name is used since system messages will not have one.
*/
const validNewChatMessage = (message): boolean =>
!!message.senderDisplayName && (message.type === 'text' || message.type === 'html');

const filledIcon = <CallWithChatCompositeIcon iconName={'ControlBarChatButtonActive'} />;
const regularIcon = <CallWithChatCompositeIcon iconName={'ControlBarChatButtonInactive'} />;

/**
* @private
*/
export const ChatButtonWithUnreadMessagesBadge = (props: ChatButtonWithUnreadMessagesBadgeProps): JSX.Element => {
const { chatAdapter, isChatPaneVisible, newMessageLabel } = props;
const [unreadChatMessagesCount, setUnreadChatMessagesCount] = useState<number>(0);
const { newMessageLabel, unreadChatMessagesCount, hideUnreadChatMessagesBadge } = props;

const baseIcon = props.showLabel ? regularIcon : filledIcon;
const callWithChatStrings = useCallWithChatCompositeStrings();
Expand All @@ -61,30 +50,13 @@ export const ChatButtonWithUnreadMessagesBadge = (props: ChatButtonWithUnreadMes
const notificationOnIcon = useCallback((): JSX.Element => {
return (
<Stack styles={chatNotificationContainerStyles}>
{unreadChatMessagesCount > 0 && (
{unreadChatMessagesCount > 0 && !hideUnreadChatMessagesBadge && (
<NotificationIcon chatMessagesCount={unreadChatMessagesCount} label={newMessageLabel} />
)}
{baseIcon}
</Stack>
);
}, [unreadChatMessagesCount, newMessageLabel, baseIcon]);

useEffect(() => {
if (isChatPaneVisible) {
setUnreadChatMessagesCount(0);
return;
}
const incrementUnreadChatMessagesCount = (event: { message: ChatMessage }): void => {
if (!isChatPaneVisible && validNewChatMessage(event.message)) {
setUnreadChatMessagesCount(unreadChatMessagesCount + 1);
}
};
chatAdapter.on('messageReceived', incrementUnreadChatMessagesCount);

return () => {
chatAdapter.off('messageReceived', incrementUnreadChatMessagesCount);
};
}, [chatAdapter, setUnreadChatMessagesCount, isChatPaneVisible, unreadChatMessagesCount]);
}, [unreadChatMessagesCount, newMessageLabel, baseIcon, hideUnreadChatMessagesBadge]);

return (
<ChatButton
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { useEffect, useState } from 'react';
import { ChatAdapter } from '../../ChatComposite/adapter/ChatAdapter';
import { ChatMessage } from '@azure/communication-chat';

/**
* Used by the CallWithChatComposite to track unread messages for showing as a badge on the Chat Button.
* @private
*/
export const useUnreadMessagesTracker = (chatAdapter: ChatAdapter, isChatPaneVisible: boolean): number => {
const [unreadChatMessagesCount, setUnreadChatMessagesCount] = useState<number>(0);

useEffect(() => {
if (isChatPaneVisible) {
setUnreadChatMessagesCount(0);
return;
}
const incrementUnreadChatMessagesCount = (event: { message: ChatMessage }): void => {
if (!isChatPaneVisible && validNewChatMessage(event.message)) {
setUnreadChatMessagesCount(unreadChatMessagesCount + 1);
}
};
chatAdapter.on('messageReceived', incrementUnreadChatMessagesCount);

return () => {
chatAdapter.off('messageReceived', incrementUnreadChatMessagesCount);
};
}, [chatAdapter, setUnreadChatMessagesCount, isChatPaneVisible, unreadChatMessagesCount]);

return unreadChatMessagesCount;
};

/**
* Helper function to determine if the message in the event is a valid one from a user.
* Display name is used since system messages will not have one.
*/
const validNewChatMessage = (message): boolean =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would this break with CTE users that have unnamed participant? (Due to cte bug and no graph integration with calling sdk yet).
cc @PorterNan

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great spot! I won't change in this PR as this function was just moved but I think you're right!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!!message.senderDisplayName && (message.type === 'text' || message.type === 'html');