diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js index 2899f5364d3..cae4c1c07a2 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js @@ -8,7 +8,7 @@ // *************************************************************** // Stage: @prod -// Group: @channels @messaging +// Group: @channels @messaging @collapsed_reply_threads import timeouts from '@/fixtures/timeouts'; @@ -177,6 +177,57 @@ describe('Messaging', () => { }); }); + it('MM-T4261_3 One-Click Reactions in Global Threads view show 3 emojis', () => { + // # Re-enable emoji picker (MM-T4261_2 disables it) and configure CRT server-side + cy.apiAdminLogin(); + cy.apiUpdateConfig({ + ServiceSettings: { + EnableEmojiPicker: true, + ThreadAutoFollow: true, + CollapsedThreads: 'default_off', + }, + }); + + // # Create a fresh user with CRT turned on so threads appear in the Threads view + cy.apiCreateUser({prefix: 'crtUser'}).then(({user: crtUser}) => { + cy.apiAddUserToTeam(testTeam.id, crtUser.id); + cy.apiSaveCRTPreference(crtUser.id, 'on'); + cy.apiLogin(crtUser); + }); + + cy.visit(offTopicPath); + + // # Enable one-click reactions for this user + cy.uiOpenSettingsModal('Display').within(() => { + cy.findByText('Display', {timeout: timeouts.ONE_MIN}).click(); + cy.findByText('Quick reactions on messages').click(); + cy.findByLabelText('On').click(); + cy.uiSaveAndClose(); + }); + + // # Post a root message and a reply so a followed thread exists + cy.apiGetChannelByName(testTeam.name, 'off-topic').then(({channel}) => { + cy.apiCreatePost(channel.id, 'Root post for Global Threads emoji test', '', {}).then((rootResp) => { + const rootPostId = rootResp.body.id; + + cy.apiCreatePost(channel.id, 'Reply to follow the thread', rootPostId, {}); + + // # Navigate to Global Threads + cy.uiClickSidebarItem('threads'); + + // # Click the thread to open the full-width thread panel + cy.get('div.ThreadItem').should('have.lengthOf.at.least', 1).first().click(); + + // * Root post is visible in the thread pane + cy.get(`#rhsPost_${rootPostId}`).should('be.visible'); + + // * Hovering over the post in Global Threads shows 3 quick reaction emojis — + // the same count as the center channel, not the 1 shown in a narrow RHS sidebar. + validateQuickReactions(rootPostId, 'GLOBAL_THREADS', defaultEmojis); + }); + }); + }); + function validateQuickReactions(postId, location, emojis) { let idPrefix; let numReactions = 3; @@ -186,7 +237,7 @@ describe('Messaging', () => { } else if (location === 'RHS_ROOT' || location === 'RHS_COMMENT') { idPrefix = 'rhsPost'; numReactions = 1; - } else if (location === 'RHS_EXPANDED') { + } else if (location === 'GLOBAL_THREADS') { idPrefix = 'rhsPost'; } diff --git a/webapp/channels/src/components/post/index.tsx b/webapp/channels/src/components/post/index.tsx index 7a4c6a9fbab..4eac3dde910 100644 --- a/webapp/channels/src/components/post/index.tsx +++ b/webapp/channels/src/components/post/index.tsx @@ -32,6 +32,7 @@ import {closeRightHandSide, selectPost, setRhsExpanded, selectPostCard, selectPo import {getBurnOnReadDurationMinutes} from 'selectors/burn_on_read'; import {isBurnOnReadPost, shouldDisplayConcealedPlaceholder} from 'selectors/burn_on_read_posts'; import {getShortcutReactToLastPostEmittedFrom, getOneClickReactionEmojis} from 'selectors/emojis'; +import {getIsGlobalThreadsView} from 'selectors/lhs'; import {getIsPostBeingEdited, getIsPostBeingEditedInRHS, isEmbedVisible} from 'selectors/posts'; import {getHighlightedPostId, getRhsState, getSelectedPostCard} from 'selectors/rhs'; import {getIsMobileView} from 'selectors/views/browser'; @@ -219,7 +220,7 @@ function makeMapStateToProps() { recentEmojis: emojis, center: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state), - isExpanded: state.views.rhs.isSidebarExpanded, + isExpanded: state.views.rhs.isSidebarExpanded || getIsGlobalThreadsView(state), isPostBeingEdited: ownProps.location === Locations.CENTER ? !getIsPostBeingEditedInRHS(state, post.id) && getIsPostBeingEdited(state, post.id) : getIsPostBeingEditedInRHS(state, post.id), isMobileView: getIsMobileView(state), previewCollapsed, diff --git a/webapp/channels/src/components/post/post_options.test.tsx b/webapp/channels/src/components/post/post_options.test.tsx new file mode 100644 index 00000000000..704e306b6f6 --- /dev/null +++ b/webapp/channels/src/components/post/post_options.test.tsx @@ -0,0 +1,127 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import type {SystemEmoji} from '@mattermost/types/emojis'; + +import {Permissions} from 'mattermost-redux/constants'; + +import {renderWithContext, screen} from 'tests/react_testing_utils'; +import {Locations} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; + +import PostOptions from './post_options'; + +// Minimal Redux state: grant ADD_REACTION to the current user so that +// ChannelPermissionGate lets the quick-reaction emoji buttons render. +const currentUserId = 'currentUser'; +const channel = TestHelper.getChannelMock({team_id: 'team1'}); +const baseState = { + entities: { + roles: { + roles: { + system_user: TestHelper.getRoleMock({permissions: [Permissions.ADD_REACTION]}), + }, + }, + users: { + currentUserId, + profiles: { + [currentUserId]: TestHelper.getUserMock({id: currentUserId, roles: 'system_user'}), + }, + }, + }, +}; + +// Proper SystemEmoji shapes — getEmojiName() reads `short_name` for system emojis. +const makeSystemEmoji = (shortName: string): SystemEmoji => ({ + name: shortName, + short_name: shortName, + short_names: [shortName], + category: 'people-body', + unified: shortName.toUpperCase(), +}); + +const post = TestHelper.getPostMock({type: '', channel_id: channel.id}); + +const baseProps = { + post, + teamId: channel.team_id, + isFlagged: false, + removePost: jest.fn(), + enableEmojiPicker: true, + isReadOnly: false, + channelIsArchived: false, + handleDropdownOpened: jest.fn(), + oneClickReactionsEnabled: true, + recentEmojis: [ + makeSystemEmoji('thumbsup'), + makeSystemEmoji('grinning'), + makeSystemEmoji('white_check_mark'), + ], + isMobileView: false, + location: Locations.RHS_ROOT as keyof typeof Locations, + pluginActions: [], + isChannelAutotranslated: false, + actions: { + emitShortcutReactToLastPostFrom: jest.fn(), + }, +}; + +describe('PostOptions - quick reaction count (MM-68681)', () => { + test('CENTER location always shows 3 quick reaction emojis', () => { + renderWithContext( + , + baseState, + ); + + expect(screen.getAllByTestId('post-menu__item_emoji')).toHaveLength(3); + }); + + test('RHS_ROOT with isExpanded false (narrow sidebar) shows 1 quick reaction emoji', () => { + renderWithContext( + , + baseState, + ); + + expect(screen.getAllByTestId('post-menu__item_emoji')).toHaveLength(1); + }); + + test('RHS_ROOT with isExpanded true (expanded sidebar or Global Threads view) shows 3 quick reaction emojis', () => { + renderWithContext( + , + baseState, + ); + + expect(screen.getAllByTestId('post-menu__item_emoji')).toHaveLength(3); + }); + + test('RHS_COMMENT with isExpanded true shows 3 quick reaction emojis', () => { + renderWithContext( + , + baseState, + ); + + expect(screen.getAllByTestId('post-menu__item_emoji')).toHaveLength(3); + }); +}); diff --git a/webapp/channels/src/selectors/lhs.test.ts b/webapp/channels/src/selectors/lhs.test.ts index d5864b5161a..e5f5d770f4a 100644 --- a/webapp/channels/src/selectors/lhs.test.ts +++ b/webapp/channels/src/selectors/lhs.test.ts @@ -4,6 +4,7 @@ import * as PreferencesSelectors from 'mattermost-redux/selectors/entities/preferences'; import type {GlobalState} from 'types/store'; +import {LhsPage} from 'types/store/lhs'; import * as Lhs from './lhs'; @@ -26,6 +27,41 @@ describe('Selectors.Lhs', () => { state = {}; }); + describe('getIsGlobalThreadsView', () => { + it('returns true when currentStaticPageId is the Threads page', () => { + state = { + views: { + lhs: { + currentStaticPageId: LhsPage.Threads, + }, + }, + }; + expect(Lhs.getIsGlobalThreadsView(state as GlobalState)).toBe(true); + }); + + it('returns false when currentStaticPageId is a different static page', () => { + state = { + views: { + lhs: { + currentStaticPageId: LhsPage.Drafts, + }, + }, + }; + expect(Lhs.getIsGlobalThreadsView(state as GlobalState)).toBe(false); + }); + + it('returns false when no static page is active (channel view)', () => { + state = { + views: { + lhs: { + currentStaticPageId: '', + }, + }, + }; + expect(Lhs.getIsGlobalThreadsView(state as GlobalState)).toBe(false); + }); + }); + describe('should return the open state of the sidebar menu', () => { [true, false].forEach((expected) => { it(`when open is ${expected}`, () => { diff --git a/webapp/channels/src/selectors/lhs.ts b/webapp/channels/src/selectors/lhs.ts index 479abd9561c..7c202185581 100644 --- a/webapp/channels/src/selectors/lhs.ts +++ b/webapp/channels/src/selectors/lhs.ts @@ -11,6 +11,7 @@ import {makeGetDraftsCount} from 'selectors/drafts'; import type {SidebarSize} from 'components/resizable_sidebar/constants'; import type {GlobalState} from 'types/store'; +import {LhsPage} from 'types/store/lhs'; import type {StaticPage} from 'types/store/lhs'; export function getIsLhsOpen(state: GlobalState): boolean { @@ -25,6 +26,10 @@ export function getCurrentStaticPageId(state: GlobalState): string { return state.views.lhs.currentStaticPageId; } +export function getIsGlobalThreadsView(state: GlobalState): boolean { + return state.views.lhs.currentStaticPageId === LhsPage.Threads; +} + export const getDraftsCount = makeGetDraftsCount(); export const getVisibleStaticPages = createSelector(