MM-63056 Migrate thread menu to new component and update label

This commit is contained in:
Harrison Healey
2025-07-11 15:57:35 -04:00
parent a17c387ff2
commit a9b4373c8a
11 changed files with 303 additions and 411 deletions
@@ -51,26 +51,7 @@ exports[`components/threading/global_threads/thread_item should report total num
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
/>
</div>
<div
className="preview"
@@ -205,26 +186,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
/>
</div>
<div
className="preview"
@@ -357,26 +319,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
/>
</div>
<div
className="preview"
@@ -15,7 +15,8 @@
&:hover,
&:focus,
&:focus-within {
&:focus-within,
&:has([aria-expanded="true"]) {
background: rgba(var(--center-channel-color-rgb), 0.04);
.alt-visible {
@@ -7,7 +7,6 @@ import type {MouseEvent, KeyboardEvent} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {DotsVerticalIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
import {PostPriority} from '@mattermost/types/posts';
@@ -27,12 +26,10 @@ import {getIsMobileView} from 'selectors/views/browser';
import Markdown from 'components/markdown';
import {makeGetMentionKeysForPost} from 'components/post_markdown';
import PriorityBadge from 'components/post_priority/post_priority_badge';
import Button from 'components/threading/common/button';
import Timestamp from 'components/timestamp';
import CRTListTutorialTip from 'components/tours/crt_tour/crt_list_tutorial_tip';
import Tag from 'components/widgets/tag/tag';
import Avatars from 'components/widgets/users/avatars';
import WithTooltip from 'components/with_tooltip';
import {CrtTutorialSteps, Preferences} from 'utils/constants';
import * as Utils from 'utils/utils';
@@ -256,27 +253,7 @@ function ThreadItem({
isFollowing={isFollowing ?? false}
hasUnreads={Boolean(newReplies)}
unreadTimestamp={unreadTimestamp}
>
<WithTooltip
title={(
<FormattedMessage
id='threading.threadItem.menu'
defaultMessage='Actions'
/>
)}
>
<Button
marginTop={true}
className='Button___icon'
aria-label={formatMessage({
id: 'threading.threadItem.menu',
defaultMessage: 'Actions',
})}
>
<DotsVerticalIcon size={18}/>
</Button>
</WithTooltip>
</ThreadMenu>
/>
</div>
{/* The strange interaction here where we need a click/keydown handler messes with the ESLint rules, so we just disable it */}
@@ -1,87 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/threading/common/thread_menu should match snapshot 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
stopPropagationOnToggle={true}
>
<button>
test
</button>
<Menu
ariaLabel="Actions"
openLeft={true}
>
<MenuItemAction
extraText="You will be notified about replies"
onClick={[Function]}
show={true}
text="Follow thread"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Open in channel"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Mark as unread"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Save"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Copy link"
/>
</Menu>
</MenuWrapper>
`;
exports[`components/threading/common/thread_menu should match snapshot after opening 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
stopPropagationOnToggle={true}
>
<button>
test
</button>
<Menu
ariaLabel="Actions"
openLeft={true}
>
<MenuItemAction
extraText="You will be notified about replies"
onClick={[Function]}
show={true}
text="Follow thread"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Open in channel"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Mark as unread"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Save"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Copy link"
/>
</Menu>
</MenuWrapper>
`;
@@ -1,8 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
.ThreadMenu {
.MenuItem__help-text {
margin: 0;
}
}
@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import set from 'lodash/set';
import React from 'react';
import type {ComponentProps} from 'react';
@@ -14,13 +12,11 @@ import {
} from 'actions/post_actions';
import {manuallyMarkThreadAsUnread} from 'actions/views/threads';
import Menu from 'components/widgets/menu/menu';
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
import {fakeDate} from 'tests/helpers/date';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {copyToClipboard} from 'utils/utils';
import type {GlobalState} from 'types/store';
import ThreadMenu from '../thread_menu';
jest.mock('mattermost-redux/actions/threads');
@@ -46,162 +42,245 @@ jest.mock('../../hooks', () => {
});
const mockDispatch = jest.fn();
let mockState: GlobalState;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
}));
describe('components/threading/common/thread_menu', () => {
let props: ComponentProps<typeof ThreadMenu>;
const baseState = {
entities: {
preferences: {myPreferences: {}},
teams: {currentTeamId: 'tid'},
general: {config: {}},
users: {currentUserId: 'uid'},
},
views: {
browser: {
windowSize: 'desktopView',
},
},
};
beforeEach(() => {
props = {
threadId: '1y8hpek81byspd4enyk9mp1ncw',
unreadTimestamp: 1610486901110,
hasUnreads: false,
isFollowing: false,
children: (
<button>{'test'}</button>
),
};
mockState = {entities: {preferences: {myPreferences: {}}}} as GlobalState;
});
test('should match snapshot', () => {
const wrapper = shallow(
test('should render thread menu button', () => {
renderWithContext(
<ThreadMenu
{...props}
/>,
baseState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByRole('button', {name: 'More Actions'})).toBeInTheDocument();
});
test('should match snapshot after opening', () => {
const wrapper = shallow(
test('should open menu when button is clicked', async () => {
renderWithContext(
<ThreadMenu
{...props}
/>,
baseState,
);
wrapper.find('button').simulate('click');
expect(wrapper).toMatchSnapshot();
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
expect(screen.getByRole('menuitem', {name: /Follow thread/})).toBeInTheDocument();
expect(screen.getByRole('menuitem', {name: /Open in channel/})).toBeInTheDocument();
expect(screen.getByRole('menuitem', {name: /Mark as unread/})).toBeInTheDocument();
expect(screen.getByRole('menuitem', {name: /Save/})).toBeInTheDocument();
expect(screen.getByRole('menuitem', {name: /Copy link/})).toBeInTheDocument();
});
test('should allow following', () => {
const wrapper = shallow(
test('should allow following', async () => {
renderWithContext(
<ThreadMenu
{...props}
isFollowing={false}
/>,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Follow thread'}).simulate('click');
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', true);
expect(mockDispatch).toHaveBeenCalledTimes(1);
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const followButton = await screen.findByRole('menuitem', {name: /Follow thread/});
await userEvent.click(followButton);
await waitFor(() => {
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', true);
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
});
test('should allow unfollowing', () => {
const wrapper = shallow(
test('should allow unfollowing', async () => {
renderWithContext(
<ThreadMenu
{...props}
isFollowing={true}
/>,
baseState,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Unfollow thread'}).simulate('click');
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', false);
expect(mockDispatch).toHaveBeenCalledTimes(1);
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const unfollowButton = screen.getByRole('menuitem', {name: /Unfollow thread/});
await userEvent.click(unfollowButton);
await waitFor(() => {
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', false);
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
});
test('should allow opening in channel', () => {
const wrapper = shallow(
test('should allow opening in channel', async () => {
renderWithContext(
<ThreadMenu
{...props}
/>,
baseState,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Open in channel'}).simulate('click');
expect(mockRouting.goToInChannel).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).not.toHaveBeenCalled();
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const openInChannelButton = screen.getByRole('menuitem', {name: /Open in channel/});
await userEvent.click(openInChannelButton);
await waitFor(() => {
expect(mockRouting.goToInChannel).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).not.toHaveBeenCalled();
});
});
test('should allow marking as read', () => {
test('should allow marking as read', async () => {
const resetFakeDate = fakeDate(new Date(1612582579566));
const wrapper = shallow(
renderWithContext(
<ThreadMenu
{...props}
hasUnreads={true}
/>,
baseState,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Mark as read'}).simulate('click');
expect(markLastPostInThreadAsUnread).not.toHaveBeenCalled();
expect(updateThreadRead).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
expect(mockDispatch).toHaveBeenCalledTimes(2);
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const markAsReadButton = screen.getByRole('menuitem', {name: /Mark as read/});
await userEvent.click(markAsReadButton);
await waitFor(() => {
expect(markLastPostInThreadAsUnread).not.toHaveBeenCalled();
expect(updateThreadRead).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
expect(mockDispatch).toHaveBeenCalledTimes(2);
});
resetFakeDate();
});
test('should allow marking as unread', () => {
const wrapper = shallow(
test('should allow marking as unread', async () => {
renderWithContext(
<ThreadMenu
{...props}
hasUnreads={false}
/>,
baseState,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Mark as unread'}).simulate('click');
expect(updateThreadRead).not.toHaveBeenCalled();
expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw');
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1610486901110);
expect(mockDispatch).toHaveBeenCalledTimes(2);
});
test('should allow saving', () => {
const wrapper = shallow(
<ThreadMenu
{...props}
/>,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Save'}).simulate('click');
expect(savePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
test('should allow unsaving', () => {
set(mockState, 'entities.preferences.myPreferences', {
'flagged_post--1y8hpek81byspd4enyk9mp1ncw': {
user_id: 'uid',
category: 'flagged_post',
name: '1y8hpek81byspd4enyk9mp1ncw',
value: 'true',
},
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const markAsUnreadButton = screen.getByRole('menuitem', {name: /Mark as unread/});
await userEvent.click(markAsUnreadButton);
await waitFor(() => {
expect(updateThreadRead).not.toHaveBeenCalled();
expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw');
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1610486901110);
expect(mockDispatch).toHaveBeenCalledTimes(2);
});
const wrapper = shallow(
<ThreadMenu
{...props}
/>,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Unsave'}).simulate('click');
expect(unsavePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
test('should allow link copying', () => {
const wrapper = shallow(
test('should allow saving', async () => {
renderWithContext(
<ThreadMenu
{...props}
/>,
baseState,
);
wrapper.find('button').simulate('click');
wrapper.find(Menu.ItemAction).find({text: 'Copy link'}).simulate('click');
expect(copyToClipboard).toHaveBeenCalledWith('http://localhost:8065/team-name-1/pl/1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).not.toHaveBeenCalled();
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const saveButton = screen.getByRole('menuitem', {name: /Save/});
await userEvent.click(saveButton);
await waitFor(() => {
expect(savePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
});
test('should allow unsaving', async () => {
renderWithContext(
<ThreadMenu
{...props}
/>,
mergeObjects(baseState, {
entities: {
preferences: {
myPreferences: {
'flagged_post--1y8hpek81byspd4enyk9mp1ncw': {
user_id: 'uid',
category: 'flagged_post',
name: '1y8hpek81byspd4enyk9mp1ncw',
value: 'true',
},
},
},
},
}),
);
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const unsaveButton = screen.getByRole('menuitem', {name: /Unsave/});
await userEvent.click(unsaveButton);
await waitFor(() => {
expect(unsavePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
});
test('should allow link copying', async () => {
renderWithContext(
<ThreadMenu
{...props}
/>,
baseState,
);
const menuButton = screen.getByRole('button', {name: 'More Actions'});
await userEvent.click(menuButton);
const copyLinkButton = screen.getByRole('menuitem', {name: /Copy link/});
await userEvent.click(copyLinkButton);
await waitFor(() => {
expect(copyToClipboard).toHaveBeenCalledWith('http://localhost:8065/team-name-1/pl/1y8hpek81byspd4enyk9mp1ncw');
expect(mockDispatch).not.toHaveBeenCalled();
});
});
});
@@ -2,10 +2,10 @@
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import type {ReactNode} from 'react';
import {useIntl} from 'react-intl';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {DotsVerticalIcon} from '@mattermost/compass-icons/components';
import type {UserThread} from '@mattermost/types/threads';
import {setThreadFollow, updateThreadRead, markLastPostInThreadAsUnread} from 'mattermost-redux/actions/threads';
@@ -17,8 +17,7 @@ import {
} from 'actions/post_actions';
import {manuallyMarkThreadAsUnread} from 'actions/views/threads';
import Menu from 'components/widgets/menu/menu';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import * as Menu from 'components/menu';
import {useReadout} from 'hooks/useReadout';
import {getSiteURL} from 'utils/url';
@@ -28,13 +27,10 @@ import type {GlobalState} from 'types/store';
import {useThreadRouting} from '../../hooks';
import './thread_menu.scss';
type Props = {
threadId: UserThread['id'];
isFollowing?: boolean;
hasUnreads: boolean;
children: ReactNode;
unreadTimestamp: number;
};
@@ -43,7 +39,6 @@ function ThreadMenu({
isFollowing = false,
unreadTimestamp,
hasUnreads,
children,
}: Props) {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
@@ -85,106 +80,131 @@ function ThreadMenu({
]);
return (
<MenuWrapper
stopPropagationOnToggle={true}
<Menu.Container
menuButton={{
id: `thread-menu-${threadId}`,
class: 'btn btn-icon btn-sm',
'aria-label': formatMessage({
id: 'threading.threadHeader.menu',
defaultMessage: 'More Actions',
}),
children: (
<DotsVerticalIcon size={18}/>
),
}}
menuButtonTooltip={{
text: formatMessage({
id: 'threading.threadHeader.menu',
defaultMessage: 'More Actions',
}),
}}
menu={{
id: `thread-menu-dropdown-${threadId}`,
}}
>
{children}
<Menu
ariaLabel={formatMessage({
id: 'threading.threadItem.menu',
defaultMessage: 'Actions',
})}
openLeft={true}
>
<Menu.ItemAction
{...isFollowing ? {
text: formatMessage({
id: 'threading.threadMenu.unfollow',
defaultMessage: 'Unfollow thread',
}),
extraText: formatMessage({
id: 'threading.threadMenu.unfollowExtra',
defaultMessage: 'You won’t be notified about replies',
}),
} : {
text: formatMessage({
id: 'threading.threadMenu.follow',
defaultMessage: 'Follow thread',
}),
extraText: formatMessage({
id: 'threading.threadMenu.followExtra',
defaultMessage: 'You will be notified about replies',
}),
}}
onClick={useCallback(() => {
dispatch(setThreadFollow(currentUserId, currentTeamId, threadId, !isFollowing));
readAloud(isFollowing ? formatMessage({
id: 'threading.threadMenu.unfollowed',
defaultMessage: 'Unfollowed thread',
}) : formatMessage({
id: 'threading.threadMenu.followed',
defaultMessage: 'Followed thread',
}));
}, [currentUserId, currentTeamId, threadId, isFollowing, setThreadFollow, readAloud, formatMessage])}
/>
<Menu.ItemAction
text={formatMessage({
id: 'threading.threadMenu.openInChannel',
defaultMessage: 'Open in channel',
})}
onClick={useCallback(() => {
goToInChannel(threadId);
readAloud(formatMessage({
id: 'threading.threadMenu.openingChannel',
defaultMessage: 'Opening channel',
}));
}, [threadId, readAloud, formatMessage])}
/>
<Menu.ItemAction
text={hasUnreads ? formatMessage({
id: 'threading.threadMenu.markRead',
defaultMessage: 'Mark as read',
<Menu.Item
labels={isFollowing ? (
<>
<FormattedMessage
id='threading.threadMenu.unfollow'
defaultMessage='Unfollow thread'
/>
<FormattedMessage
id='threading.threadMenu.unfollowExtra'
defaultMessage='You won’t be notified about replies'
/>
</>
) : (
<>
<FormattedMessage
id='threading.threadMenu.follow'
defaultMessage='Follow thread'
/>
<FormattedMessage
id='threading.threadMenu.followExtra'
defaultMessage='You will be notified about replies'
/>
</>)
}
onClick={useCallback(() => {
dispatch(setThreadFollow(currentUserId, currentTeamId, threadId, !isFollowing));
readAloud(isFollowing ? formatMessage({
id: 'threading.threadMenu.unfollowed',
defaultMessage: 'Unfollowed thread',
}) : formatMessage({
id: 'threading.threadMenu.markUnread',
defaultMessage: 'Mark as unread',
})}
onClick={handleReadUnread}
/>
<Menu.ItemAction
text={isSaved ? formatMessage({
id: 'threading.threadMenu.unsave',
defaultMessage: 'Unsave',
id: 'threading.threadMenu.followed',
defaultMessage: 'Followed thread',
}));
}, [currentUserId, currentTeamId, threadId, isFollowing, setThreadFollow, readAloud, formatMessage])}
/>
<Menu.Item
labels={
<FormattedMessage
id='threading.threadMenu.openInChannel'
defaultMessage='Open in channel'
/>
}
onClick={useCallback(() => {
goToInChannel(threadId);
readAloud(formatMessage({
id: 'threading.threadMenu.openingChannel',
defaultMessage: 'Opening channel',
}));
}, [threadId, readAloud, formatMessage])}
/>
<Menu.Item
labels={hasUnreads ? (
<FormattedMessage
id='threading.threadMenu.markRead'
defaultMessage='Mark as read'
/>
) : (
<FormattedMessage
id='threading.threadMenu.markUnread'
defaultMessage='Mark as unread'
/>
)}
onClick={handleReadUnread}
/>
<Menu.Item
labels={isSaved ? (
<FormattedMessage
id='threading.threadMenu.unsave'
defaultMessage='Unsave'
/>
) : (
<FormattedMessage
id='threading.threadMenu.save'
defaultMessage='Save'
/>
)}
onClick={useCallback(() => {
dispatch(isSaved ? unsavePost(threadId) : savePost(threadId));
readAloud(isSaved ? formatMessage({
id: 'threading.threadMenu.unsaved',
defaultMessage: 'Unsaved',
}) : formatMessage({
id: 'threading.threadMenu.save',
defaultMessage: 'Save',
})}
onClick={useCallback(() => {
dispatch(isSaved ? unsavePost(threadId) : savePost(threadId));
readAloud(isSaved ? formatMessage({
id: 'threading.threadMenu.unsaved',
defaultMessage: 'Unsaved',
}) : formatMessage({
id: 'threading.threadMenu.saved',
defaultMessage: 'Saved',
}));
}, [threadId, isSaved])}
/>
<Menu.ItemAction
text={formatMessage({
id: 'threading.threadMenu.copy',
defaultMessage: 'Copy link',
})}
onClick={useCallback(() => {
copyToClipboard(`${getSiteURL()}/${team}/pl/${threadId}`);
readAloud(formatMessage({
id: 'threading.threadMenu.linkCopied',
defaultMessage: 'Link copied',
}));
}, [team, threadId])}
/>
</Menu>
</MenuWrapper>
id: 'threading.threadMenu.saved',
defaultMessage: 'Saved',
}));
}, [threadId, isSaved])}
/>
<Menu.Item
labels={
<FormattedMessage
id='threading.threadMenu.copy'
defaultMessage='Copy link'
/>
}
onClick={useCallback(() => {
copyToClipboard(`${getSiteURL()}/${team}/pl/${threadId}`);
readAloud(formatMessage({
id: 'threading.threadMenu.linkCopied',
defaultMessage: 'Link copied',
}));
}, [team, threadId])}
/>
</Menu.Container>
);
}
@@ -45,19 +45,7 @@ exports[`components/threading/global_threads/thread_pane should match snapshot 1
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title="More Actions"
>
<Memo(Button)
className="Button___icon Button___large"
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</Memo(ThreadMenu)>
/>
</React.Fragment>
}
/>
@@ -13,6 +13,7 @@
justify-content: space-between;
padding: 12px 16px;
border-bottom: var(--border-default);
gap: 4px;
grid-area: header;
--button-separator-height: 24px;
@@ -55,14 +56,6 @@
font-weight: 400;
}
}
.MenuWrapper {
margin-left: 4px;
.dropdown-menu {
min-width: 250px;
}
}
}
.ThreadViewer {
@@ -6,7 +6,6 @@ import type {ReactNode} from 'react';
import {useIntl} from 'react-intl';
import {useSelector, useDispatch} from 'react-redux';
import {DotsVerticalIcon} from '@mattermost/compass-icons/components';
import type {UserThread} from '@mattermost/types/threads';
import {setThreadFollow} from 'mattermost-redux/actions/threads';
@@ -14,7 +13,6 @@ import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getPost, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
import Header from 'components/widgets/header';
import WithTooltip from 'components/with_tooltip';
import type {GlobalState} from 'types/store';
@@ -118,18 +116,7 @@ const ThreadPane = ({
isFollowing={isFollowing}
hasUnreads={Boolean(thread.unread_replies || thread.unread_mentions)}
unreadTimestamp={unreadTimestamp}
>
<WithTooltip
title={formatMessage({
id: 'threading.threadHeader.menu',
defaultMessage: 'More Actions',
})}
>
<Button className='Button___icon Button___large'>
<DotsVerticalIcon size={18}/>
</Button>
</WithTooltip>
</ThreadMenu>
/>
</>
)}
/>
-1
View File
@@ -5762,7 +5762,6 @@
"threading.numReplies": "{totalReplies, plural, =0 {Reply} =1 {# reply} other {# replies}}",
"threading.threadHeader.menu": "More Actions",
"threading.threadItem.ariaLabel": "Thread by {author}",
"threading.threadItem.menu": "Actions",
"threading.threadItem.timestamp": "Last reply ",
"threading.threadList.markRead": "Mark all threads as read",
"threading.threadList.tabsLabel": "Filter visible threads",