Skip to content

Commit

Permalink
feat: control ReactionsSelector dialog display
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela committed Sep 6, 2024
1 parent d9f3709 commit af6b94b
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 542 deletions.
21 changes: 2 additions & 19 deletions src/components/Message/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo } from 'react';

import {
useActionHandler,
Expand All @@ -10,7 +10,6 @@ import {
useMuteHandler,
useOpenThreadHandler,
usePinHandler,
useReactionClick,
useReactionHandler,
useReactionsFetcher,
useRetryHandler,
Expand Down Expand Up @@ -44,14 +43,10 @@ type MessageContextPropsToPick =
| 'handleReaction'
| 'handleFetchReactions'
| 'handleRetry'
| 'isReactionEnabled'
| 'mutes'
| 'onMentionsClickMessage'
| 'onMentionsHoverMessage'
| 'onReactionListClick'
| 'reactionSelectorRef'
| 'reactionDetailsSort'
| 'showDetailedReactions'
| 'sortReactions'
| 'sortReactionDetails';

Expand Down Expand Up @@ -218,8 +213,6 @@ export const Message = <
const { addNotification } = useChannelActionContext<StreamChatGenerics>('Message');
const { highlightedMessageId, mutes } = useChannelStateContext<StreamChatGenerics>('Message');

const reactionSelectorRef = useRef<HTMLDivElement | null>(null);

const handleAction = useActionHandler(message);
const handleOpenThread = useOpenThreadHandler(message, propOpenThread);
const handleReaction = useReactionHandler(message);
Expand Down Expand Up @@ -264,20 +257,14 @@ export const Message = <
notify: addNotification,
});

const { isReactionEnabled, onReactionListClick, showDetailedReactions } = useReactionClick(
message,
reactionSelectorRef,
undefined,
closeReactionSelectorOnClick,
);

const highlighted = highlightedMessageId === message.id;

return (
<MemoizedMessage
additionalMessageInputProps={props.additionalMessageInputProps}
autoscrollToBottom={props.autoscrollToBottom}
canPin={canPin}
closeReactionSelectorOnClick={closeReactionSelectorOnClick}
customMessageActions={props.customMessageActions}
disableQuotedMessages={props.disableQuotedMessages}
endOfGroup={props.endOfGroup}
Expand All @@ -297,7 +284,6 @@ export const Message = <
handleRetry={handleRetry}
highlighted={highlighted}
initialMessage={props.initialMessage}
isReactionEnabled={isReactionEnabled}
lastReceivedId={props.lastReceivedId}
message={message}
Message={props.Message}
Expand All @@ -306,15 +292,12 @@ export const Message = <
mutes={mutes}
onMentionsClickMessage={onMentionsClick}
onMentionsHoverMessage={onMentionsHover}
onReactionListClick={onReactionListClick}
onUserClick={props.onUserClick}
onUserHover={props.onUserHover}
pinPermissions={props.pinPermissions}
reactionDetailsSort={reactionDetailsSort}
reactionSelectorRef={reactionSelectorRef}
readBy={props.readBy}
renderText={props.renderText}
showDetailedReactions={showDetailedReactions}
sortReactionDetails={sortReactionDetails}
sortReactions={sortReactions}
threadList={props.threadList}
Expand Down
36 changes: 15 additions & 21 deletions src/components/Message/MessageOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
ThreadIcon as DefaultThreadIcon,
} from './icons';
import { MESSAGE_ACTIONS } from './utils';

import { MessageActions } from '../MessageActions';

import { useTranslationContext } from '../../context';
import { MessageContextValue, useMessageContext } from '../../context/MessageContext';

import type { DefaultStreamChatGenerics, IconProps } from '../../types/types';
import { useTranslationContext } from '../../context';
import { ReactionSelectorWithButton } from '../Reactions/ReactionSelectorWithButton';
import { useDialogIsOpen } from '../Dialog';
import clsx from 'clsx';

export type MessageOptionsProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand All @@ -21,8 +23,6 @@ export type MessageOptionsProps<
ActionsIcon?: React.ComponentType<IconProps>;
/* If true, show the `ThreadIcon` and enable navigation into a `Thread` component. */
displayReplies?: boolean;
/* React mutable ref that can be placed on the message root `div` of MessageActions component */
messageWrapperRef?: React.RefObject<HTMLDivElement>;
/* Custom component rendering the icon used in a button invoking reactions selector for a given message. */
ReactionIcon?: React.ComponentType<IconProps>;
/* Theme string to be added to CSS class names. */
Expand All @@ -40,7 +40,6 @@ const UnMemoizedMessageOptions = <
ActionsIcon = DefaultActionsIcon,
displayReplies = true,
handleOpenThread: propHandleOpenThread,
messageWrapperRef,
ReactionIcon = DefaultReactionIcon,
theme = 'simple',
ThreadIcon = DefaultThreadIcon,
Expand All @@ -51,13 +50,12 @@ const UnMemoizedMessageOptions = <
handleOpenThread: contextHandleOpenThread,
initialMessage,
message,
onReactionListClick,
showDetailedReactions,
threadList,
} = useMessageContext<StreamChatGenerics>('MessageOptions');

const { t } = useTranslationContext('MessageOptions');

const messageActionsDialogIsOpen = useDialogIsOpen(`message-actions--${message.id}`);
const reactionSelectorDialogIsOpen = useDialogIsOpen(`reaction-selector--${message.id}`);
const handleOpenThread = propHandleOpenThread || contextHandleOpenThread;

const messageActions = getMessageActions();
Expand All @@ -78,11 +76,15 @@ const UnMemoizedMessageOptions = <
return null;
}

const rootClassName = `str-chat__message-${theme}__actions str-chat__message-options`;

return (
<div className={rootClassName} data-testid='message-options'>
<MessageActions ActionsIcon={ActionsIcon} messageWrapperRef={messageWrapperRef} />
<div
className={clsx(`str-chat__message-${theme}__actions str-chat__message-options`, {
'str-chat__message-options--active':
messageActionsDialogIsOpen || reactionSelectorDialogIsOpen,
})}
data-testid='message-options'
>
<MessageActions ActionsIcon={ActionsIcon} />
{shouldShowReplies && (
<button
aria-label={t('aria/Open Thread')}
Expand All @@ -94,15 +96,7 @@ const UnMemoizedMessageOptions = <
</button>
)}
{shouldShowReactions && (
<button
aria-expanded={showDetailedReactions}
aria-label={t('aria/Open Reaction Selector')}
className={`str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--reactions str-chat__message-reactions-button`}
data-testid='message-reaction-action'
onClick={onReactionListClick}
>
<ReactionIcon className='str-chat__message-action-icon' />
</button>
<ReactionSelectorWithButton ReactionIcon={ReactionIcon} theme={theme} />
)}
</div>
);
Expand Down
23 changes: 4 additions & 19 deletions src/components/Message/MessageSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
import { EditMessageForm as DefaultEditMessageForm, MessageInput } from '../MessageInput';
import { MML } from '../MML';
import { Modal } from '../Modal';
import {
ReactionsList as DefaultReactionList,
ReactionSelector as DefaultReactionSelector,
} from '../Reactions';
import { ReactionsList as DefaultReactionList } from '../Reactions';
import { MessageBounceModal } from '../MessageBounce/MessageBounceModal';

import { useChatContext } from '../../context/ChatContext';
Expand Down Expand Up @@ -59,13 +56,10 @@ const MessageSimpleWithContext = <
handleRetry,
highlighted,
isMyMessage,
isReactionEnabled,
message,
onUserClick,
onUserHover,
reactionSelectorRef,
renderText,
showDetailedReactions,
threadList,
} = props;

Expand All @@ -83,7 +77,7 @@ const MessageSimpleWithContext = <
MessageRepliesCountButton = DefaultMessageRepliesCountButton,
MessageStatus = DefaultMessageStatus,
MessageTimestamp = DefaultMessageTimestamp,
ReactionSelector = DefaultReactionSelector,

ReactionsList = DefaultReactionList,
PinIndicator,
} = useComponentContext<StreamChatGenerics>('MessageSimple');
Expand All @@ -100,14 +94,6 @@ const MessageSimpleWithContext = <
return <MessageDeleted message={message} />;
}

/** FIXME: isReactionEnabled should be removed with next major version and a proper centralized permissions logic should be put in place
* With the current permissions implementation it would be sth like:
* const messageActions = getMessageActions();
* const canReact = messageActions.includes(MESSAGE_ACTIONS.react);
*/
const canReact = isReactionEnabled;
const canShowReactions = hasReactions;

const showMetadata = !groupedByUser || endOfGroup;
const showReplyCountButton = !threadList && !!message.reply_count;
const allowRetry = message.status === 'failed' && message.errorStatusCode !== 403;
Expand Down Expand Up @@ -136,7 +122,7 @@ const MessageSimpleWithContext = <
'str-chat__message--has-attachment': hasAttachment,
'str-chat__message--highlighted': highlighted,
'str-chat__message--pinned pinned-message': message.pinned,
'str-chat__message--with-reactions': canShowReactions,
'str-chat__message--with-reactions': hasReactions,
'str-chat__message-send-can-be-retried':
message?.status === 'failed' && message?.errorStatusCode !== 403,
'str-chat__message-with-thread-link': showReplyCountButton,
Expand Down Expand Up @@ -190,8 +176,7 @@ const MessageSimpleWithContext = <
>
<MessageOptions />
<div className='str-chat__message-reactions-host'>
{canShowReactions && <ReactionsList reverse />}
{showDetailedReactions && canReact && <ReactionSelector ref={reactionSelectorRef} />}
{hasReactions && <ReactionsList reverse />}
</div>
<div className='str-chat__message-bubble'>
{message.attachments?.length && !message.quoted_message ? (
Expand Down
83 changes: 82 additions & 1 deletion src/components/Message/__tests__/MessageOptions.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable jest-dom/prefer-to-have-class */
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import { Message } from '../Message';
Expand All @@ -22,6 +22,7 @@ import {
getTestClientWithUser,
} from '../../../mock-builders';
import { DialogsManagerProvider } from '../../../context';
import { defaultReactionOptions } from '../../Reactions';

const MESSAGE_ACTIONS_TEST_ID = 'message-actions';

Expand Down Expand Up @@ -73,6 +74,7 @@ async function renderMessageOptions({
onReactionListClick={customMessageProps?.onReactionListClick}
/>
),
reactionOptions: defaultReactionOptions,
}}
>
<Message {...defaultMessageProps} {...customMessageProps}>
Expand Down Expand Up @@ -182,6 +184,85 @@ describe('<MessageOptions />', () => {
expect(queryByTestId(reactionActionTestId)).not.toBeInTheDocument();
});

it('should not render ReactionsSelector until open', async () => {
const { queryByTestId } = await renderMessageOptions({
channelStateOpts: {
channelCapabilities: { 'send-reaction': true },
},
});
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
await act(async () => {
await fireEvent.click(queryByTestId(reactionActionTestId));
});
expect(screen.getByTestId('reaction-selector')).toBeInTheDocument();
});

it('should unmount ReactionsSelector when closed by click on dialog overlay', async () => {
const { queryByTestId } = await renderMessageOptions({
channelStateOpts: {
channelCapabilities: { 'send-reaction': true },
},
});
await act(async () => {
await fireEvent.click(queryByTestId(reactionActionTestId));
});
await act(async () => {
await fireEvent.click(screen.getByTestId('str-chat__dialog-overlay'));
});
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
});

it('should unmount ReactionsSelector when closed pressed Esc button', async () => {
const { queryByTestId } = await renderMessageOptions({
channelStateOpts: {
channelCapabilities: { 'send-reaction': true },
},
});
await act(async () => {
await fireEvent.click(queryByTestId(reactionActionTestId));
});
await act(async () => {
await fireEvent.keyUp(document, { charCode: 27, code: 'Escape', key: 'Escape' });
});
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
});

it('should unmount ReactionsSelector when closed on reaction selection and closeReactionSelectorOnClick enabled', async () => {
const { queryByTestId } = await renderMessageOptions({
channelStateOpts: {
channelCapabilities: { 'send-reaction': true },
},
customMessageProps: {
closeReactionSelectorOnClick: true,
},
});
await act(async () => {
await fireEvent.click(queryByTestId(reactionActionTestId));
});
await act(async () => {
await fireEvent.click(screen.queryAllByTestId('select-reaction-button')[0]);
});
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
});

it('should not unmount ReactionsSelector when closed on reaction selection and closeReactionSelectorOnClick enabled', async () => {
const { queryByTestId } = await renderMessageOptions({
channelStateOpts: {
channelCapabilities: { 'send-reaction': true },
},
customMessageProps: {
closeReactionSelectorOnClick: false,
},
});
await act(async () => {
await fireEvent.click(queryByTestId(reactionActionTestId));
});
await act(async () => {
await fireEvent.click(screen.queryAllByTestId('select-reaction-button')[0]);
});
expect(screen.queryByTestId('reaction-selector')).toBeInTheDocument();
});

it('should render message actions', async () => {
const { queryByTestId } = await renderMessageOptions({
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
Expand Down
9 changes: 6 additions & 3 deletions src/components/Message/__tests__/QuotedMessage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ChannelStateProvider,
ChatProvider,
ComponentProvider,
DialogsManagerProvider,
TranslationProvider,
} from '../../../context';
import {
Expand Down Expand Up @@ -65,9 +66,11 @@ async function renderQuotedMessage(customProps) {
Message: () => <MessageSimple channelConfig={channelConfig} />,
}}
>
<Message {...customProps}>
<QuotedMessage {...customProps} />
</Message>
<DialogsManagerProvider id='quoted-message-dialogs-manager-provider'>
<Message {...customProps}>
<QuotedMessage {...customProps} />
</Message>
</DialogsManagerProvider>
</ComponentProvider>
</TranslationProvider>
</ChannelActionProvider>
Expand Down
Loading

0 comments on commit af6b94b

Please sign in to comment.