Skip to content

Commit

Permalink
feat: Reactions UI Improvements (#16433)
Browse files Browse the repository at this point in the history
* feat: Reactions UI Improvements

* test fixes

* fixes

* variable rename

* cr fixes

* feat: improve message body width (#16435)
  • Loading branch information
przemvs authored Jan 2, 2024
1 parent de98d97 commit c9a1023
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 113 deletions.
5 changes: 3 additions & 2 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,11 @@
"conversationLabelFavorites": "Favorites",
"conversationLabelGroups": "Groups",
"conversationLabelPeople": "People",
"conversationLikesCaptionPlural": "{{number}} people",
"conversationLikesCaptionPluralMoreThan2": "[bold]{{userNames}}[/bold] and [showmore]{{number}} more[/showmore]",
"conversationLikesCaptionPlural": "[bold]{{firstUser}}[/bold] and [bold]{{secondUser}}[/bold]",
"conversationLikesCaptionReactedPlural": "reacted with {{emojiName}}",
"conversationLikesCaptionReactedSingular": "reacted with {{emojiName}}",
"conversationLikesCaptionSingular": "{{number}} person",
"conversationLikesCaptionSingular": "[bold]{{userName}}[/bold]",
"conversationLocationLink": "Open Map",
"conversationMLSMigrationFinalisationOngoingCall": "Due to migration to MLS, you might have issues with your current call. If that's the case, hang up and call again.",
"conversationMemberJoined": "[bold]{{name}}[/bold] added {{users}} to the conversation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ export const ContentMessageComponent: React.FC<ContentMessageProps> = ({
{timeAgo}
</MessageTime>
</span>

<ReadReceiptStatus
message={message}
is1to1Conversation={conversation.is1to1()}
isLastDeliveredMessage={isLastDeliveredMessage}
/>
</MessageHeader>
)}
<div className="message-body">
Expand Down Expand Up @@ -239,14 +245,6 @@ export const ContentMessageComponent: React.FC<ContentMessageProps> = ({
isRemovedFromConversation={conversation.removed_from_conversation()}
/>
)}

<div className="message-body-actions">
<ReadReceiptStatus
message={message}
is1to1Conversation={conversation.is1to1()}
isLastDeliveredMessage={isLastDeliveredMessage}
/>
</div>
</div>

<MessageReactionsList
Expand All @@ -257,6 +255,7 @@ export const ContentMessageComponent: React.FC<ContentMessageProps> = ({
onTooltipReactionCountClick={() => onClickReactionDetails(message)}
onLastReactionKeyEvent={() => setActionMenuVisibility(false)}
isRemovedFromConversation={conversation.removed_from_conversation()}
users={conversation.allUserEntities()}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {useMessageFocusedTabIndex} from 'Components/MessagesList/Message/util';
import {getEmojiTitleFromEmojiUnicode} from 'Util/EmojiUtil';
import {isTabKey} from 'Util/KeyboardUtil';
import {t} from 'Util/LocalizerUtil';
import {replaceReactComponents} from 'Util/LocalizerUtil/ReactLocalizerUtil';

import {EmojiChar} from './EmojiChar';
import {
Expand All @@ -35,12 +36,14 @@ import {
messageReactionButtonTooltipText,
messageReactionButtonTooltipTextLink,
messageReactionCount,
userBoldStyle,
} from './MessageReactions.styles';

import {User} from '../../../../../../entity/User';

export interface EmojiPillProps {
emoji: string;
emojiUnicode: string;
emojiCount: number;
handleReactionClick: (emoji: string) => void;
isMessageFocused: boolean;
onTooltipReactionCountClick: () => void;
Expand All @@ -49,12 +52,14 @@ export interface EmojiPillProps {
index: number;
emojiListCount: number;
hasUserReacted: boolean;
reactingUsers: User[];
}

const MAX_USER_NAMES_TO_SHOW = 2;

export const EmojiPill: FC<EmojiPillProps> = ({
emoji,
emojiUnicode,
emojiCount,
handleReactionClick,
isMessageFocused,
onTooltipReactionCountClick,
Expand All @@ -63,32 +68,74 @@ export const EmojiPill: FC<EmojiPillProps> = ({
index,
emojiListCount,
hasUserReacted,
reactingUsers,
}) => {
const messageFocusedTabIndex = useMessageFocusedTabIndex(isMessageFocused);
const [isOpen, setTooltipVisibility] = useState(false);
const emojiName = getEmojiTitleFromEmojiUnicode(emojiUnicode);
const isActive = hasUserReacted && !isRemovedFromConversation;

const emojiCount = reactingUsers.length;

const showTooltip = () => {
setTooltipVisibility(true);
};

const hideTooltip = () => {
setTooltipVisibility(false);
};

const reactingUserNames = reactingUsers.slice(0, MAX_USER_NAMES_TO_SHOW).map(user => user.name());

const conversationReactionCaption = () => {
if (emojiCount > MAX_USER_NAMES_TO_SHOW) {
return t('conversationLikesCaptionPluralMoreThan2', {
number: (emojiCount - MAX_USER_NAMES_TO_SHOW).toString(),
userNames: reactingUserNames.join(', '),
});
}

if (emojiCount === MAX_USER_NAMES_TO_SHOW) {
return t('conversationLikesCaptionPlural', {
firstUser: reactingUserNames[0],
secondUser: reactingUserNames[1],
});
}

return t('conversationLikesCaptionSingular', {userName: reactingUserNames?.[0] || ''});
};

const caption = conversationReactionCaption();

const content = replaceReactComponents(caption, [
{
start: '<strong>',
end: '</strong>',
render: text => (
<strong key={text} css={userBoldStyle}>
{text}
</strong>
),
},
{
start: '[showmore]',
end: '[/showmore]',
render: text => (
<button key={text} onClick={onTooltipReactionCountClick} css={messageReactionButtonTooltipTextLink}>
{text}
</button>
),
},
]);

return (
<div onMouseEnter={showTooltip} onMouseLeave={hideTooltip} onFocus={showTooltip} onBlur={hideTooltip}>
<Tooltip
body={
<div css={messageReactionButtonTooltip}>
<EmojiChar styles={messageReactionButtonTooltipImage} emoji={emoji} />
<p css={messageReactionButtonTooltipText}>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
<span onClick={onTooltipReactionCountClick} css={messageReactionButtonTooltipTextLink}>
{emojiCount > 1
? t('conversationLikesCaptionPlural', {number: emojiCount.toString()})
: t('conversationLikesCaptionSingular', {number: emojiCount.toString()})}
</span>{' '}
{content}{' '}
{emojiCount > 1
? t('conversationLikesCaptionReactedPlural', {emojiName})
: t('conversationLikesCaptionReactedSingular', {emojiName})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,18 @@ import {CSSObject} from '@emotion/react';
export const messageReactionWrapper: CSSObject = {
display: 'flex',
gap: '0.5rem',
paddingLeft: '56px',
paddingLeft: 'var(--conversation-message-sender-width)',
flexWrap: 'wrap',
maxWidth: '100%',
marginRight: 'var(--conversation-message-timestamp-width)',
'.tooltip-content': {marginBottom: '0 !important'},
'.tooltip-content': {
backgroundColor: 'var(--gray-95) !important',
marginBottom: '0 !important',
padding: '6px 8px !important',
'.tooltip-arrow': {
borderTopColor: 'var(--gray-95) !important',
},
},
};

export const messageReactionButton: CSSObject = {
Expand All @@ -60,17 +67,30 @@ export const messageReactionButton: CSSObject = {
userSelect: 'none',
};

export const messageReactionButtonTooltip: CSSObject = {display: 'flex', maxWidth: 130, whiteSpace: 'break-spaces'};
export const messageReactionButtonTooltipImage: CSSObject = {marginRight: 8, lineHeight: '2.5em'};
export const messageReactionButtonTooltip: CSSObject = {
display: 'flex',
flexDirection: 'column',
maxWidth: 165,
whiteSpace: 'break-spaces',
};
export const messageReactionButtonTooltipImage: CSSObject = {
fontSize: 'var(--font-size-large)',
lineHeight: 'var(--line-height-md)',
};
export const messageReactionDetailsMargin: CSSObject = {marginRight: '0.4rem'};
export const reactionsCountAlignment: CSSObject = {display: 'flex', alignItems: 'center'};
export const messageReactionButtonTooltipText: CSSObject = {fontSize: '0.7rem'};
export const messageReactionButtonTooltipText: CSSObject = {fontSize: '0.7rem', marginTop: '8px'};
export const messageReactionButtonTooltipTextLink: CSSObject = {
color: 'var(--blue-500)',
background: 'transparent',
cursor: 'pointer',
fontWeight: 600,
padding: 0,
border: 'none',
textDecoration: 'underline',
};

export const userBoldStyle: CSSObject = {fontWeight: 700};

export const messageReactionCount = (isActive?: boolean): CSSObject => {
return {
color: isActive ? 'var(--accent-color)' : 'var(--message-reactions-count)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ import {generateQualifiedId} from 'test/helper/UserGenerator';

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

const user1 = generateQualifiedId();
const user2 = generateQualifiedId();
const user3 = generateQualifiedId();
import {User} from '../../../../../../entity/User';

const user1 = new User();
const user2 = new User();
const user3 = new User();
const reactions: ReactionMap = [
['😇', [user1, user2, user3]],
['😊', [user1, user2]],
['👍', [user2]],
['😉', [user2]],
['😇', [user1.qualifiedId, user2.qualifiedId, user3.qualifiedId]],
['😊', [user1.qualifiedId, user2.qualifiedId]],
['👍', [user2.qualifiedId]],
['😉', [user2.qualifiedId]],
];

const defaultProps: MessageReactionsListProps = {
Expand All @@ -43,6 +45,7 @@ const defaultProps: MessageReactionsListProps = {
onLastReactionKeyEvent: jest.fn(),
isRemovedFromConversation: false,
selfUserId: generateQualifiedId(),
users: [user1, user2, user3],
};

describe('MessageReactionsList', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
*
*/

import {FC} from 'react';

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

import {ReactionMap} from 'src/script/storage';
Expand All @@ -28,6 +26,8 @@ import {matchQualifiedIds} from 'Util/QualifiedId';
import {EmojiPill} from './EmojiPill';
import {messageReactionWrapper} from './MessageReactions.styles';

import {User} from '../../../../../../entity/User';

export interface MessageReactionsListProps {
reactions: ReactionMap;
handleReactionClick: (emoji: string) => void;
Expand All @@ -36,10 +36,11 @@ export interface MessageReactionsListProps {
onTooltipReactionCountClick: () => void;
onLastReactionKeyEvent: () => void;
isRemovedFromConversation: boolean;
users: User[];
}

const MessageReactionsList: FC<MessageReactionsListProps> = ({reactions, ...props}) => {
const {selfUserId, ...emojiPillProps} = props;
const MessageReactionsList = ({reactions, ...props}: MessageReactionsListProps) => {
const {selfUserId, users: conversationUsers, ...emojiPillProps} = props;

return (
<div css={messageReactionWrapper} data-uie-name="message-reactions">
Expand All @@ -48,9 +49,13 @@ const MessageReactionsList: FC<MessageReactionsListProps> = ({reactions, ...prop
const emojiListCount = users.length;
const hasUserReacted = users.some(user => matchQualifiedIds(selfUserId, user));

const reactingUsers = users
.map(qualifiedId => conversationUsers.find(user => matchQualifiedIds(qualifiedId, user.qualifiedId)))
.filter((user): user is User => typeof user !== 'undefined');

return (
<EmojiPill
emojiCount={users.length}
reactingUsers={reactingUsers}
hasUserReacted={hasUserReacted}
emojiUnicode={emojiUnicode}
emoji={emoji}
Expand Down
4 changes: 1 addition & 3 deletions src/script/components/MessagesList/Message/PingMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,8 @@ const PingMessage: React.FC<PingMessageProps> = ({message, is1to1Conversation, i
<span className="message-header-sender-name">{unsafeSenderName}</span>
<span className="ellipsis">{caption}</span>
</p>
</div>
<div className="message-body-actions">

<ReadReceiptStatus
showOnHover
message={message}
is1to1Conversation={is1to1Conversation}
isLastDeliveredMessage={isLastDeliveredMessage}
Expand Down
14 changes: 2 additions & 12 deletions src/script/components/MessagesList/Message/ReadReceiptStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,9 @@ export interface ReadReceiptStatusProps {
is1to1Conversation: boolean;
isLastDeliveredMessage: boolean;
message: Message;
showOnHover?: boolean;
}

const ReadReceiptStatus: React.FC<ReadReceiptStatusProps> = ({
message,
is1to1Conversation,
isLastDeliveredMessage,
showOnHover = false,
}) => {
const ReadReceiptStatus: React.FC<ReadReceiptStatusProps> = ({message, is1to1Conversation, isLastDeliveredMessage}) => {
const [readReceiptText, setReadReceiptText] = useState('');
const {readReceipts} = useKoSubscribableChildren(message, ['readReceipts']);

Expand All @@ -62,11 +56,7 @@ const ReadReceiptStatus: React.FC<ReadReceiptStatusProps> = ({
)}
{showEyeIndicator && (
<div
className={cx(
'message-status-read',
is1to1Conversation && 'message-status-read__one-on-one',
showOnHover && 'message-status-read__show-on-hover',
)}
className={cx('message-status-read', is1to1Conversation && 'message-status-read__one-on-one')}
data-uie-name="status-message-read-receipts"
aria-label={t('accessibility.messageDetailsReadReceipts', readReceiptText)}
>
Expand Down
2 changes: 1 addition & 1 deletion src/style/common/variables.less
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ body {
@conversation-max-width: 800px;

@conversation-message-max-width: 640px;
@conversation-message-sender-width: 56px;
@conversation-message-sender-width: 64px;

body {
--conversation-message-sender-width: @conversation-message-sender-width;
Expand Down
Loading

0 comments on commit c9a1023

Please sign in to comment.