MM-62828 - Updating reactions tooltip (#34002)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Waiting to run
Web App CI / check-types (push) Waiting to run
Web App CI / test (push) Waiting to run
Web App CI / build (push) Waiting to run

* MM-62828 - Updating reactions tooltip

* Updating tests

* Updating lint

* Updating lint

* Fixing lint

* Updating test

* Updating test

* Updating test

* Updating test

* Updating tests

* Updating reaction specs

* Fixing cypress test

* Updating intl

* Updating tests

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Asaad Mahmood 2025-10-09 19:20:13 +05:00 committed by GitHub
parent 4186f0389a
commit 3de7e747ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 151 additions and 45 deletions

View file

@ -171,7 +171,7 @@ describe('Collapsed Reply Threads', () => {
// * Thumbs-up reaction displays as reaction on post
cy.get(`#${postId}_message`).within(() => {
cy.findByLabelText('reactions').should('be.visible');
cy.findByLabelText('remove reaction +1').should('be.visible');
cy.findByRole('button', {name: /reacted with :\+1:/i}).should('be.visible');
});
// * Reacting to a root post should not create a thread (thread footer should not exist)

View file

@ -33,11 +33,11 @@ export function checkReactionFromPost(postId, emoji = 'smile') {
if (postId) {
cy.get(`#${postId}_message`).within(() => {
cy.findByLabelText('reactions').should('exist');
cy.findByLabelText(`remove reaction ${emoji}`).should('exist');
cy.findByLabelText(`You reacted with :${emoji}:. Click to remove.`).should('exist');
});
} else {
cy.findByLabelText('reactions').should('exist');
cy.findByLabelText(`remove reaction ${emoji}}`).should('exist');
cy.findByLabelText(`You reacted with :${emoji}:. Click to remove.`).should('exist');
}
}

View file

@ -85,13 +85,13 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
// * Check if emoji reaction is shown in the last message in center
cy.getLastPostId().then((lastPostId) => {
cy.get(`#post_${lastPostId}`).findByLabelText('remove reaction smile').should('exist');
cy.get(`#post_${lastPostId}`).findByLabelText('You reacted with :smile:. Click to remove.').should('exist');
});
// * Check if no emoji reaction is shown from last comment both in RHS and center
cy.get('@prevLastPostId').then((lastPostId) => {
cy.get(`#rhsPost_${lastPostId}`).findByLabelText('remove reaction smile').should('not.exist');
cy.get(`#post_${lastPostId}`).findByLabelText('remove reaction smile').should('not.exist');
cy.get(`#rhsPost_${lastPostId}`).findByLabelText('You reacted with :smile:. Click to remove.').should('not.exist');
cy.get(`#post_${lastPostId}`).findByLabelText('You reacted with :smile:. Click to remove.').should('not.exist');
});
cy.uiCloseRHS();
@ -143,7 +143,7 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
// * Check if no emoji reaction is shown in the last comment at RHS
cy.get('@postInRHS').within(() => {
cy.findByLabelText('reactions').should('not.exist');
cy.findByLabelText('remove reaction smile').should('not.exist');
cy.findByLabelText('You reacted with :smile:. Click to remove.').should('not.exist');
});
cy.uiCloseRHS();
@ -234,7 +234,7 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
cy.getLastPostId().then((lastPostId) => {
cy.get(`#${lastPostId}_message`).within(() => {
cy.findByLabelText('reactions').should('not.exist');
cy.findByLabelText('remove reaction smile').should('not.exist');
cy.findByLabelText('You reacted with :smile:. Click to remove.').should('not.exist');
});
});
});

View file

@ -34,7 +34,7 @@ describe('Messaging', () => {
// * Thumbs-up reaction displays as reaction on post
cy.get(`#${postId}_message`).within(() => {
cy.findByLabelText('reactions').should('be.visible');
cy.findByLabelText('remove reaction +1').should('be.visible');
cy.findByLabelText('You reacted with :+1:. Click to remove.').should('be.visible');
});
// # Close RHS
@ -62,7 +62,7 @@ describe('Messaging', () => {
// * Emoji reaction is added to the post
cy.get(`#${postId}_message`).within(() => {
cy.findByLabelText('reactions').should('exist');
cy.findByLabelText('remove reaction upside down face').should('exist');
cy.findByLabelText('You reacted with :upside_down_face:. Click to remove.').should('exist');
});
// * Reaction appears in recently used section of emoji picker
@ -102,8 +102,8 @@ describe('Messaging', () => {
// * Two reactions are added to the message in the expanded RHS
cy.get(`#rhsPost_${postId}`).within(() => {
cy.findByLabelText('reactions').should('be.visible');
cy.findByLabelText('remove reaction smiley').should('be.visible');
cy.findByLabelText('remove reaction upside down face').should('be.visible');
cy.findByLabelText('You reacted with :smiley:. Click to remove.').should('be.visible');
cy.findByLabelText('You reacted with :upside_down_face:. Click to remove.').should('be.visible');
});
// # Close RHS

View file

@ -25,7 +25,7 @@ exports[`components/post_view/Reaction should apply read-only class if user does
}
>
<button
aria-label="react with smile"
aria-label="2 users reacted with :smile:"
className="Reaction Reaction--unreacted Reaction--read-only"
id="postReaction-post_id-smile"
onClick={[Function]}
@ -34,6 +34,8 @@ exports[`components/post_view/Reaction should apply read-only class if user does
className="d-flex align-items-center"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
@ -91,7 +93,7 @@ exports[`components/post_view/Reaction should apply read-only class if user does
}
>
<button
aria-label="react with smile"
aria-label="2 users reacted with :smile:"
className="Reaction Reaction--reacted Reaction--read-only"
id="postReaction-post_id-smile"
onClick={[Function]}
@ -100,6 +102,8 @@ exports[`components/post_view/Reaction should apply read-only class if user does
className="d-flex align-items-center"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
@ -157,7 +161,7 @@ exports[`components/post_view/Reaction should match snapshot 1`] = `
}
>
<button
aria-label="react with smile"
aria-label="2 users reacted with :smile:. Click to add."
className="Reaction Reaction--unreacted "
id="postReaction-post_id-smile"
onClick={[Function]}
@ -166,6 +170,8 @@ exports[`components/post_view/Reaction should match snapshot 1`] = `
className="d-flex align-items-center"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
@ -223,7 +229,7 @@ exports[`components/post_view/Reaction should match snapshot when a current user
}
>
<button
aria-label="remove reaction smile"
aria-label="2 users reacted with :smile:. Click to remove."
className="Reaction Reaction--reacted "
id="postReaction-post_id-smile"
onClick={[Function]}
@ -232,6 +238,8 @@ exports[`components/post_view/Reaction should match snapshot when a current user
className="d-flex align-items-center"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>

View file

@ -23,6 +23,7 @@ import {addReaction} from 'actions/post_actions';
import * as Emoji from 'utils/emoji';
import Reaction from './reaction';
import {makeGetNamesOfUsers} from './reaction_tooltip';
type Props = {
emojiName: string;
@ -40,6 +41,8 @@ function makeMapStateToProps() {
},
);
const getNamesOfUsers = makeGetNamesOfUsers();
return function mapStateToProps(state: GlobalState, ownProps: Props) {
const channelId = ownProps.post.channel_id;
@ -55,15 +58,13 @@ function makeMapStateToProps() {
if (emoji) {
emojiImageUrl = getEmojiImageUrl(emoji as EmojiType);
}
const currentUserId = getCurrentUserId(state);
return {
currentUserId,
reactionCount: ownProps.reactions.length,
canAddReactions: canAddReactions(state, channelId),
canRemoveReactions: canRemoveReactions(state, channelId),
emojiImageUrl,
currentUserReacted: didCurrentUserReact(state, ownProps.reactions),
users: getNamesOfUsers(state, ownProps.reactions),
};
};
}

View file

@ -6,11 +6,13 @@ import React from 'react';
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import Reaction from 'components/post_view/reaction/reaction';
import {Reaction as ReactionClass} from 'components/post_view/reaction/reaction';
import {getIntl} from 'utils/i18n';
import {TestHelper} from 'utils/test_helper';
describe('components/post_view/Reaction', () => {
const intl = getIntl();
const post = TestHelper.getPostMock({
id: 'post_id',
});
@ -30,12 +32,10 @@ describe('components/post_view/Reaction', () => {
getMissingProfilesByIds: jest.fn(),
removeReaction: jest.fn(),
};
const currentUserId = 'user_id_1';
const baseProps = {
canAddReactions: true,
canRemoveReactions: true,
currentUserId,
post,
currentUserReacted: false,
emojiName,
@ -43,10 +43,11 @@ describe('components/post_view/Reaction', () => {
reactions,
emojiImageUrl: 'emoji_image_url',
actions,
intl,
};
test('should match snapshot', () => {
const wrapper = shallow<Reaction>(<Reaction {...baseProps}/>);
const wrapper = shallow<ReactionClass>(<ReactionClass {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
@ -66,36 +67,34 @@ describe('components/post_view/Reaction', () => {
currentUserReacted: true,
reactions: newReactions,
};
const wrapper = shallow<Reaction>(<Reaction {...props}/>);
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should return null/empty if no emojiImageUrl', () => {
const props = {...baseProps, emojiImageUrl: ''};
const wrapper = shallow<Reaction>(<Reaction {...props}/>);
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should apply read-only class if user does not have permission to add reaction', () => {
const props = {...baseProps, canAddReactions: false};
const wrapper = shallow<Reaction>(<Reaction {...props}/>);
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should apply read-only class if user does not have permission to remove reaction', () => {
const newCurrentUserId = 'user_id_2';
const props = {
...baseProps,
canRemoveReactions: false,
currentUserId: newCurrentUserId,
currentUserReacted: true,
};
const wrapper = shallow<Reaction>(<Reaction {...props}/>);
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should have called actions.getMissingProfilesByIds when loadMissingProfiles is called', () => {
const wrapper = shallow<Reaction>(<Reaction {...baseProps}/>);
const wrapper = shallow<ReactionClass>(<ReactionClass {...baseProps}/>);
wrapper.instance().loadMissingProfiles();
expect(actions.getMissingProfilesByIds).toHaveBeenCalledTimes(1);

View file

@ -2,12 +2,12 @@
// See LICENSE.txt for license information.
import React from 'react';
import {injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import type {Post} from '@mattermost/types/posts';
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import * as Utils from 'utils/utils';
import ReactionTooltip from './reaction_tooltip';
import './reaction.scss';
@ -17,18 +17,13 @@ type State = {
reactedClass: 'Reaction--reacted' | 'Reaction--reacting' | 'Reaction--unreacted' | 'Reaction--unreacting';
};
type Props = {
type Props = WrappedComponentProps & {
/*
* The post to render the reaction for
*/
post: Post;
/*
* The user id of the logged in user
*/
currentUserId: string;
/*
* The name of the emoji for the reaction
*/
@ -81,9 +76,13 @@ type Props = {
*/
removeReaction: (postId: string, emojiName: string) => void;
};
// Names of users for tooltip-like aria label, computed via selector
users?: string[];
}
export default class Reaction extends React.PureComponent<Props, State> {
export class Reaction extends React.PureComponent<Props, State> {
private reactionButtonRef = React.createRef<HTMLButtonElement>();
private reactionCountRef = React.createRef<HTMLSpanElement>();
private animating = false;
@ -186,6 +185,8 @@ export default class Reaction extends React.PureComponent<Props, State> {
emojiName,
reactionCount,
reactions,
users = [],
intl,
} = this.props;
const {displayNumber} = this.state;
const reactedNumber = currentUserReacted ? reactionCount : reactionCount + 1;
@ -196,15 +197,110 @@ export default class Reaction extends React.PureComponent<Props, State> {
const readOnlyClass = (canAddReactions && canRemoveReactions) ? '' : 'Reaction--read-only';
const emojiNameWithSpaces = this.props.emojiName.replace(/_/g, ' ');
let ariaLabelEmoji = `${Utils.localizeMessage({id: 'reaction.reactWidth.ariaLabel', defaultMessage: 'react with'})} ${emojiNameWithSpaces}`;
if (currentUserReacted && canRemoveReactions) {
ariaLabelEmoji = `${Utils.localizeMessage({id: 'reaction.removeReact.ariaLabel', defaultMessage: 'remove reaction'})} ${emojiNameWithSpaces}`;
let ariaLabelEmoji = `${emojiNameWithSpaces}`;
// If intl/users are available, append the same sentence shown in the tooltip
if (intl) {
const otherUsersCount = reactions.length - users.length;
let names: string | undefined;
if (otherUsersCount > 0) {
if (users.length > 0) {
names = intl.formatMessage(
{
id: 'reaction.usersAndOthersReacted',
defaultMessage: '{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}',
},
{
users: users.join(', '),
otherUsers: otherUsersCount,
},
);
} else {
names = intl.formatMessage(
{
id: 'reaction.othersReacted',
defaultMessage: '{otherUsers, number} {otherUsers, plural, one {user} other {users}}',
},
{
otherUsers: otherUsersCount,
},
);
}
} else if (users.length > 1) {
names = intl.formatMessage(
{
id: 'reaction.usersReacted',
defaultMessage: '{users} and {lastUser}',
},
{
users: users.slice(0, -1).join(', '),
lastUser: users[users.length - 1],
},
);
} else {
names = users[0];
}
let reactionVerb: string;
if (users.length + otherUsersCount > 1) {
if (currentUserReacted) {
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.youAndUsers',
defaultMessage: 'reacted',
});
} else {
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.users',
defaultMessage: 'reacted',
});
}
} else if (currentUserReacted) {
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.you',
defaultMessage: 'reacted',
});
} else {
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.user',
defaultMessage: 'reacted',
});
}
const tooltipTitle = intl.formatMessage(
{
id: 'reaction.reacted',
defaultMessage: '{users} {reactionVerb} with {emoji}',
},
{
users: names,
reactionVerb,
emoji: ':' + emojiName + ':',
},
);
let tooltipHint: string | undefined;
if (currentUserReacted && canRemoveReactions) {
tooltipHint = intl.formatMessage({
id: 'reaction.a11y.clickToRemove',
defaultMessage: 'Click to remove.',
});
} else if (!currentUserReacted && canAddReactions) {
tooltipHint = intl.formatMessage({
id: 'reaction.a11y.clickToAdd',
defaultMessage: 'Click to add.',
});
}
ariaLabelEmoji = tooltipHint ? `${tooltipTitle}. ${tooltipHint}` : tooltipTitle;
}
const emojiIcon = (
<img
className='Reaction__emoji emoticon'
src={this.props.emojiImageUrl}
alt=''
aria-hidden={true}
/>
);
@ -247,3 +343,5 @@ export default class Reaction extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(Reaction);

View file

@ -7,7 +7,7 @@ exports[`components/ReactionList should render when there are reactions 1`] = `
aria-label="reactions"
className="post-reaction-list"
>
<Connect(Reaction)
<Connect(injectIntl(Reaction))
emojiName="expressionless"
key="expressionless"
post={

View file

@ -5300,6 +5300,8 @@
"reaction_limit_reached_modal.body": "Oops! It looks like we've hit a ceiling on emoji reactions for this message. Please contact your system administrator for any adjustments to this limit.",
"reaction_limit_reached_modal.body.admin": "Oops! It looks like we've hit a ceiling on emoji reactions for this message. We've <link>set a limit</link> to keep things running smoothly on your server. As a system administrator, you can adjust this limit from the <linkAdmin>system console</linkAdmin>.",
"reaction_limit_reached_modal.title": "You've reached the reaction limit",
"reaction.a11y.clickToAdd": "Click to add.",
"reaction.a11y.clickToRemove": "Click to remove.",
"reaction.add.ariaLabel": "Add a reaction",
"reaction.clickToAdd": "(click to add)",
"reaction.clickToRemove": "(click to remove)",
@ -5310,8 +5312,6 @@
"reaction.reactionVerb.users": "reacted",
"reaction.reactionVerb.you": "reacted",
"reaction.reactionVerb.youAndUsers": "reacted",
"reaction.reactWidth.ariaLabel": "react with",
"reaction.removeReact.ariaLabel": "remove reaction",
"reaction.usersAndOthersReacted": "{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}",
"reaction.usersReacted": "{users} and {lastUser}",
"reaction.you": "You",