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(