Skip to content

Commit

Permalink
feat: Sort user reactions chronologically [WPB-4253] (#16101)
Browse files Browse the repository at this point in the history
  • Loading branch information
atomrc authored Oct 24, 2023
1 parent 7a42dd6 commit 26bc2d0
Show file tree
Hide file tree
Showing 27 changed files with 452 additions and 432 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {useRelativeTimestamp} from 'src/script/hooks/useRelativeTimestamp';
import {StatusType} from 'src/script/message/StatusType';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {getMessageAriaLabel} from 'Util/conversationMessages';
import {groupByReactionUsers} from 'Util/ReactionUtil';

import {ContentAsset} from './asset';
import {MessageActionsMenu} from './MessageActions/MessageActions';
Expand Down Expand Up @@ -65,7 +64,7 @@ export interface ContentMessageProps extends Omit<MessageActions, 'onClickResetS
onClickReaction: (emoji: string) => void;
}

const ContentMessageComponent: React.FC<ContentMessageProps> = ({
export const ContentMessageComponent: React.FC<ContentMessageProps> = ({
conversation,
message,
findMessage,
Expand Down Expand Up @@ -102,18 +101,19 @@ const ContentMessageComponent: React.FC<ContentMessageProps> = ({
reactions,
status,
user,
quote,
} = useKoSubscribableChildren(message, [
'senderName',
'timestamp',
'ephemeral_caption',
'ephemeral_status',
'assets',
'other_likes',
'was_edited',
'failedToSend',
'reactions',
'status',
'user',
'quote',
]);

const shouldShowAvatar = (): boolean => {
Expand All @@ -140,9 +140,6 @@ const ContentMessageComponent: React.FC<ContentMessageProps> = ({
setActionMenuVisibility(isMessageFocused || msgFocusState);
}, [msgFocusState, isMessageFocused]);

const reactionGroupedByUser = groupByReactionUsers(reactions);
const reactionsTotalCount = Array.from(reactionGroupedByUser).length;

return (
<div
aria-label={messageAriaLabel}
Expand Down Expand Up @@ -187,10 +184,10 @@ const ContentMessageComponent: React.FC<ContentMessageProps> = ({
<EphemeralTimer message={message} />
</div>
)}
{message.quote() && (
{quote && (
<Quote
conversation={conversation}
quote={message.quote()}
quote={quote}
selfId={selfId}
findMessage={findMessage}
showDetail={onClickImage}
Expand Down Expand Up @@ -235,9 +232,8 @@ const ContentMessageComponent: React.FC<ContentMessageProps> = ({
handleActionMenuVisibility={setActionMenuVisibility}
contextMenu={contextMenu}
isMessageFocused={msgFocusState}
messageWithSection={hasMarker}
handleReactionClick={onClickReaction}
reactionsTotalCount={reactionsTotalCount}
reactionsTotalCount={reactions.length}
isRemovedFromConversation={conversation.removed_from_conversation()}
/>
)}
Expand All @@ -253,7 +249,7 @@ const ContentMessageComponent: React.FC<ContentMessageProps> = ({

<MessageReactionsList
reactions={reactions}
userId={selfId.id}
selfUserId={selfId}
handleReactionClick={onClickReaction}
isMessageFocused={msgFocusState}
onTooltipReactionCountClick={() => onClickReactionDetails(message)}
Expand All @@ -263,5 +259,3 @@ const ContentMessageComponent: React.FC<ContentMessageProps> = ({
</div>
);
};

export {ContentMessageComponent};
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const defaultProps: MessageActionsMenuProps = {
contextMenu: {entries: ko.observable([{label: 'option1', text: 'option1'}])},
isMessageFocused: true,
handleActionMenuVisibility: jest.fn(),
messageWithSection: false,
handleReactionClick: jest.fn(),
reactionsTotalCount: 0,
isRemovedFromConversation: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export interface MessageActionsMenuProps {
contextMenu: {entries: ko.Subscribable<ContextMenuEntry[]>};
isMessageFocused: boolean;
handleActionMenuVisibility: (isVisible: boolean) => void;
messageWithSection: boolean;
handleReactionClick: (emoji: string) => void;
reactionsTotalCount: number;
isRemovedFromConversation: boolean;
Expand All @@ -71,7 +70,6 @@ const MessageActionsMenu: FC<MessageActionsMenuProps> = ({
isMessageFocused,
handleActionMenuVisibility,
message,
messageWithSection,
handleReactionClick,
reactionsTotalCount,
isRemovedFromConversation,
Expand Down Expand Up @@ -181,7 +179,6 @@ const MessageActionsMenu: FC<MessageActionsMenuProps> = ({
handleKeyDown={handleKeyDown}
resetActionMenuStates={resetActionMenuStates}
wrapperRef={wrapperRef}
message={message}
handleReactionClick={handleReactionClick}
/>
{message.isReplyable() && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import React from 'react';

import {render, act, fireEvent, waitFor} from '@testing-library/react';

import {ContentMessage} from 'src/script/entity/message/ContentMessage';
import {t} from 'Util/LocalizerUtil';

import {MessageReactions, MessageReactionsProps} from './MessageReactions';
Expand All @@ -32,7 +31,6 @@ const thumbsUpEmoji = '👍';
const likeEmoji = '❤️';
const wrapperRef = React.createRef<HTMLDivElement>();
const defaultProps: MessageReactionsProps = {
message: new ContentMessage(),
handleReactionClick: jest.fn(),
messageFocusedTabIndex: 0,
currentMsgActionName: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
*
*/

import {useState, useCallback, RefObject, FC, useRef} from 'react';
import {useState, RefObject, FC, useRef} from 'react';

import {ContentMessage} from 'src/script/entity/message/ContentMessage';
import {KEY} from 'Util/KeyboardUtil';
import {t} from 'Util/LocalizerUtil';

Expand All @@ -46,7 +45,6 @@ export interface MessageReactionsProps {
handleCurrentMsgAction: (actionName: string) => void;
resetActionMenuStates: () => void;
wrapperRef: RefObject<HTMLDivElement>;
message: ContentMessage;
handleReactionClick: (emoji: string) => void;
}

Expand All @@ -58,7 +56,6 @@ const MessageReactions: FC<MessageReactionsProps> = ({
handleKeyDown,
resetActionMenuStates,
wrapperRef,
message,
handleReactionClick,
}) => {
const isThumbUpAction = currentMsgActionName === MessageActionsId.THUMBSUP;
Expand All @@ -84,93 +81,78 @@ const MessageReactions: FC<MessageReactionsProps> = ({
setShowEmojis(false);
};

const handleReactionCurrentState = useCallback(
(actionName = '') => {
const isActive = !!actionName;
handleCurrentMsgAction(actionName);
handleMenuOpen(isActive);
setShowEmojis(isActive);
},
[handleCurrentMsgAction, handleMenuOpen],
);
const handleReactionCurrentState = (actionName = '') => {
const isActive = !!actionName;
handleCurrentMsgAction(actionName);
handleMenuOpen(isActive);
setShowEmojis(isActive);
};

const handleEmojiBtnClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
const selectedMsgActionName = event.currentTarget.dataset.uieName;
if (currentMsgActionName === selectedMsgActionName) {
// reset on double click
handleReactionCurrentState('');
} else if (selectedMsgActionName) {
handleReactionCurrentState(selectedMsgActionName);
showReactions(event.currentTarget.getBoundingClientRect());
}
};

const handleEmojiBtnClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
const selectedMsgActionName = event.currentTarget.dataset.uieName;
const handleEmojiKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
event.stopPropagation();
const selectedMsgActionName = event.currentTarget.dataset.uieName;
handleKeyDown(event);
if ([KEY.SPACE, KEY.ENTER].includes(event.key)) {
if (currentMsgActionName === selectedMsgActionName) {
// reset on double click
handleReactionCurrentState('');
} else if (selectedMsgActionName) {
handleReactionCurrentState(selectedMsgActionName);
showReactions(event.currentTarget.getBoundingClientRect());
}
},
[currentMsgActionName, handleReactionCurrentState],
);

const handleEmojiKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLButtonElement>) => {
event.stopPropagation();
const selectedMsgActionName = event.currentTarget.dataset.uieName;
handleKeyDown(event);
if ([KEY.SPACE, KEY.ENTER].includes(event.key)) {
if (currentMsgActionName === selectedMsgActionName) {
// reset on double click
handleReactionCurrentState('');
} else if (selectedMsgActionName) {
handleReactionCurrentState(selectedMsgActionName);
showReactions(event.currentTarget.getBoundingClientRect());
}
}
},
[currentMsgActionName, handleKeyDown, handleReactionCurrentState],
);
}
};

const showReactions = (rect: DOMRect) => {
setPOSX(rect.x);
setPOSY(rect.y);
};

const handleMsgActionClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
const actionType = event.currentTarget.dataset.uieName;
switch (actionType) {
case MessageActionsId.EMOJI:
handleEmojiBtnClick(event);
break;
case MessageActionsId.THUMBSUP:
toggleActiveMenu(event);
handleReactionClick(thumbsUpEmoji);
break;
case MessageActionsId.HEART:
toggleActiveMenu(event);
handleReactionClick(likeEmoji);
break;
}
},
[handleEmojiBtnClick, handleReactionClick, toggleActiveMenu],
);
const handleMsgActionClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
const actionType = event.currentTarget.dataset.uieName;
switch (actionType) {
case MessageActionsId.EMOJI:
handleEmojiBtnClick(event);
break;
case MessageActionsId.THUMBSUP:
toggleActiveMenu(event);
handleReactionClick(thumbsUpEmoji);
break;
case MessageActionsId.HEART:
toggleActiveMenu(event);
handleReactionClick(likeEmoji);
break;
}
};

const handleMsgActionKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLButtonElement>) => {
event.stopPropagation();
const actionType = event.currentTarget.dataset.uieName;
switch (actionType) {
case MessageActionsId.EMOJI:
handleEmojiKeyDown(event);
break;
case MessageActionsId.THUMBSUP:
handleKeyDown(event);
break;
case MessageActionsId.HEART:
handleKeyDown(event);
break;
}
},
[handleEmojiKeyDown, handleKeyDown],
);
const handleMsgActionKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
event.stopPropagation();
const actionType = event.currentTarget.dataset.uieName;
switch (actionType) {
case MessageActionsId.EMOJI:
handleEmojiKeyDown(event);
break;
case MessageActionsId.THUMBSUP:
handleKeyDown(event);
break;
case MessageActionsId.HEART:
handleKeyDown(event);
break;
}
};

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@
import {render, fireEvent, within} from '@testing-library/react';

import {withTheme} from 'src/script/auth/util/test/TestUtil';
import {createUuid} from 'Util/uuid';
import {ReactionMap} from 'src/script/storage';
import {generateQualifiedId} from 'test/helper/UserGenerator';

import {MessageReactionsList, MessageReactionsListProps} from './MessageReactionsList';

const reactions = {
'1': '😇,😊',
'2': '😊,👍,😉,😇',
'3': '😇',
};
const user1 = generateQualifiedId();
const user2 = generateQualifiedId();
const user3 = generateQualifiedId();
const reactions: ReactionMap = [
['😇', [user1, user2, user3]],
['😊', [user1, user2]],
['👍', [user2]],
['😉', [user2]],
];

const defaultProps: MessageReactionsListProps = {
reactions: reactions,
Expand All @@ -37,7 +42,7 @@ const defaultProps: MessageReactionsListProps = {
isMessageFocused: false,
onLastReactionKeyEvent: jest.fn(),
isRemovedFromConversation: false,
userId: createUuid(),
selfUserId: generateQualifiedId(),
};

describe('MessageReactionsList', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,34 @@

import {FC} from 'react';

import type {QualifiedId} from '@wireapp/api-client/lib/user/';

import {ReactionMap} from 'src/script/storage';
import {getEmojiUnicode} from 'Util/EmojiUtil';
import {Reactions, groupByReactionUsers, sortReactionsByUserCount} from 'Util/ReactionUtil';
import {matchQualifiedIds} from 'Util/QualifiedId';

import {EmojiPill} from './EmojiPill';
import {messageReactionWrapper} from './MessageReactions.styles';

export interface MessageReactionsListProps {
reactions: Reactions;
reactions: ReactionMap;
handleReactionClick: (emoji: string) => void;
userId: string;
selfUserId: QualifiedId;
isMessageFocused: boolean;
onTooltipReactionCountClick: () => void;
onLastReactionKeyEvent: () => void;
isRemovedFromConversation: boolean;
}

const MessageReactionsList: FC<MessageReactionsListProps> = ({reactions, ...props}) => {
const reactionGroupedByUser = groupByReactionUsers(reactions);
const reactionsGroupedByUserArray = Array.from(reactionGroupedByUser);
const reactionsList =
reactionsGroupedByUserArray.length > 1
? sortReactionsByUserCount(reactionsGroupedByUserArray)
: reactionsGroupedByUserArray;
const {userId, ...emojiPillProps} = props;
const {selfUserId, ...emojiPillProps} = props;

return (
<div css={messageReactionWrapper} data-uie-name="message-reactions">
{reactionsList.map(([emoji, users], index) => {
{reactions.map(([emoji, users], index) => {
const emojiUnicode = getEmojiUnicode(emoji);
const emojiListCount = reactionsList.length;
const hasUserReacted = users.includes(userId);
const emojiListCount = users.length;
const hasUserReacted = users.some(user => matchQualifiedIds(selfUserId, user));

return (
<EmojiPill
Expand Down
Loading

0 comments on commit 26bc2d0

Please sign in to comment.