MM-57119 Migrate various components to use WithTooltip (#26458)

* MM-57119 Migrate MultiSelectCard to use WithTooltip

* MM-57119 Migrate SeatsCalculator to use WithTooltip

* MM-57119 Migrate BillingSummary and RenewalCard to WithTooltip

* Add getEmojiName helper function

* Add emojiStyle=large option for WithTooltip

* MM-57119 Migrate PostRecentReactions to use WithTooltip

* MM-57119 Migrate Reaction to use WithTooltip

* MM-57119 Migrate PostReaction to use WithTooltip

* MM-57119 Migrate ReactionList to use WithTooltip

* Change how WithTooltip's shortcut prop works to take a sequence of keys

* Convert TeamButton to functional component

* MM-57119 Migrate TeamButton to use WithTooltip

* MM-57119 Migrate ChannelFilter to use WithTooltip

* Revert unintentional change

* Finish removing values prop from KeyboardShortcutSequence

* Address feedback

* Add margin between emoji and text
Этот коммит содержится в:
Harrison Healey
2024-03-18 14:39:09 -04:00
коммит произвёл GitHub
родитель 8c3ab07d49
Коммит 297135c5b4
38 изменённых файлов: 772 добавлений и 929 удалений

Просмотреть файл

@@ -7,6 +7,7 @@ import {Preferences as ReduxPreferences} from 'mattermost-redux/constants';
import {getCustomEmojisByName as selectCustomEmojisByName, getCustomEmojisEnabled} from 'mattermost-redux/selectors/entities/emojis'; import {getCustomEmojisByName as selectCustomEmojisByName, getCustomEmojisEnabled} from 'mattermost-redux/selectors/entities/emojis';
import {get} from 'mattermost-redux/selectors/entities/preferences'; import {get} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import {getEmojiMap, getRecentEmojisData, getRecentEmojisNames, isCustomEmojiEnabled} from 'selectors/emojis'; import {getEmojiMap, getRecentEmojisData, getRecentEmojisNames, isCustomEmojiEnabled} from 'selectors/emojis';
import {isCustomStatusEnabled, makeGetCustomStatus} from 'selectors/views/custom_status'; import {isCustomStatusEnabled, makeGetCustomStatus} from 'selectors/views/custom_status';
@@ -68,16 +69,13 @@ export function addRecentEmojis(aliases) {
let updatedRecentEmojis = [...recentEmojis]; let updatedRecentEmojis = [...recentEmojis];
for (const alias of aliases) { for (const alias of aliases) {
let name;
const emoji = emojiMap.get(alias); const emoji = emojiMap.get(alias);
if (!emoji) { if (!emoji) {
continue; continue;
} else if (emoji.short_name) {
name = emoji.short_name;
} else {
name = emoji.name;
} }
const name = getEmojiName(emoji);
const currentEmojiIndexInRecentList = updatedRecentEmojis.findIndex((recentEmoji) => recentEmoji.name === name); const currentEmojiIndexInRecentList = updatedRecentEmojis.findIndex((recentEmoji) => recentEmoji.name === name);
if (currentEmojiIndexInRecentList > -1) { if (currentEmojiIndexInRecentList > -1) {
const currentEmojiInRecentList = updatedRecentEmojis[currentEmojiIndexInRecentList]; const currentEmojiInRecentList = updatedRecentEmojis[currentEmojiIndexInRecentList];

Просмотреть файл

@@ -11,10 +11,6 @@
} }
} }
#BillingSubscriptions__seatOverageTooltip {
margin-left: -5px;
}
.BillingSubscriptions__tooltip.tooltip { .BillingSubscriptions__tooltip.tooltip {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
@@ -67,18 +63,6 @@
} }
} }
.BillingSubscriptions__tooltipTitle {
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.BillingSubscriptions__tooltipMessage {
margin-top: 8px;
font-size: 14px;
line-height: 20px;
}
.UpgradeMattermostCloud { .UpgradeMattermostCloud {
text-align: center; text-align: center;
} }

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {FormattedDate, FormattedMessage, FormattedNumber} from 'react-intl'; import {FormattedDate, FormattedMessage, FormattedNumber, defineMessages} from 'react-intl';
import {useDispatch} from 'react-redux'; import {useDispatch} from 'react-redux';
import {CheckCircleOutlineIcon, CheckIcon, ClockOutlineIcon} from '@mattermost/compass-icons/components'; import {CheckCircleOutlineIcon, CheckIcon, ClockOutlineIcon} from '@mattermost/compass-icons/components';
@@ -18,11 +18,21 @@ import CloudInvoicePreview from 'components/cloud_invoice_preview';
import EmptyBillingHistorySvg from 'components/common/svg_images_components/empty_billing_history_svg'; import EmptyBillingHistorySvg from 'components/common/svg_images_components/empty_billing_history_svg';
import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg'; import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
import OverlayTrigger from 'components/overlay_trigger'; import WithTooltip from 'components/with_tooltip';
import Tooltip from 'components/tooltip';
import {BillingSchemes, CloudLinks, TrialPeriodDays, ModalIdentifiers} from 'utils/constants'; import {BillingSchemes, CloudLinks, TrialPeriodDays, ModalIdentifiers} from 'utils/constants';
const messages = defineMessages({
partialChargesTooltipTitle: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges',
defaultMessage: 'What are partial charges?',
},
partialChargesTooltipText: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message',
defaultMessage: 'Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.',
},
});
export const noBillingHistory = ( export const noBillingHistory = (
<div className='BillingSummary__noBillingHistory'> <div className='BillingSummary__noBillingHistory'>
<EmptyBillingHistorySvg <EmptyBillingHistorySvg
@@ -180,6 +190,7 @@ type InvoiceInfoProps = {
export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasMore, willRenew}: InvoiceInfoProps) => { export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasMore, willRenew}: InvoiceInfoProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isUpcomingInvoice = invoice?.status.toLowerCase() === 'upcoming'; const isUpcomingInvoice = invoice?.status.toLowerCase() === 'upcoming';
const openInvoicePreview = () => { const openInvoicePreview = () => {
dispatch( dispatch(
@@ -298,32 +309,14 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM
id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges' id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges'
defaultMessage='Partial charges' defaultMessage='Partial charges'
/> />
<OverlayTrigger <WithTooltip
delayShow={500} id='BillingSubscriptions__seatOverageTooltip'
title={messages.partialChargesTooltipTitle}
hint={messages.partialChargesTooltipText}
placement='bottom' placement='bottom'
overlay={
<Tooltip
id='BillingSubscriptions__seatOverageTooltip'
className='BillingSubscriptions__tooltip BillingSubscriptions__tooltip-right'
positionLeft={390}
>
<div className='BillingSubscriptions__tooltipTitle'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges'
defaultMessage='What are partial charges?'
/>
</div>
<div className='BillingSubscriptions__tooltipMessage'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message'
defaultMessage='Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.'
/>
</div>
</Tooltip>
}
> >
<i className='icon-information-outline'/> <i className='icon-information-outline'/>
</OverlayTrigger> </WithTooltip>
</div> </div>
{partialCharges.map((charge: any) => ( {partialCharges.map((charge: any) => (
<div <div

Просмотреть файл

@@ -238,7 +238,7 @@ describe('components/AdvancedCreateComment', () => {
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), getBoundingClientRect: jest.fn(), focus: jest.fn()}; (wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), getBoundingClientRect: jest.fn(), focus: jest.fn()};
wrapper.instance().handleEmojiClick({name: 'smile'} as any); wrapper.instance().handleEmojiClick(TestHelper.getCustomEmojiMock({name: 'smile'}));
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT); jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);
expect(onUpdateCommentDraft).toHaveBeenCalled(); expect(onUpdateCommentDraft).toHaveBeenCalled();
@@ -252,7 +252,7 @@ describe('components/AdvancedCreateComment', () => {
wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test', uploadsInProgress: [], fileInfos: []}), wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test', uploadsInProgress: [], fileInfos: []}),
caretPosition: 'test'.length, // cursor is at the end caretPosition: 'test'.length, // cursor is at the end
}); });
wrapper.instance().handleEmojiClick({name: 'smile'} as any); wrapper.instance().handleEmojiClick(TestHelper.getCustomEmojiMock({name: 'smile'}));
// Message with no space at the end // Message with no space at the end
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT); jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);
@@ -264,7 +264,7 @@ describe('components/AdvancedCreateComment', () => {
wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test ', uploadsInProgress: [], fileInfos: []}), wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test ', uploadsInProgress: [], fileInfos: []}),
caretPosition: 'test '.length, // cursor is at the end caretPosition: 'test '.length, // cursor is at the end
}); });
wrapper.instance().handleEmojiClick({name: 'smile'} as any); wrapper.instance().handleEmojiClick(TestHelper.getCustomEmojiMock({name: 'smile'}));
// Message with space at the end // Message with space at the end
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT); jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);

Просмотреть файл

@@ -15,6 +15,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {Posts} from 'mattermost-redux/constants'; import {Posts} from 'mattermost-redux/constants';
import type {ActionResult} from 'mattermost-redux/types/actions'; import type {ActionResult} from 'mattermost-redux/types/actions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import {sortFileInfos} from 'mattermost-redux/utils/file_utils'; import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
import * as GlobalActions from 'actions/global_actions'; import * as GlobalActions from 'actions/global_actions';
@@ -471,7 +472,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
}; };
handleEmojiClick = (emoji: Emoji) => { handleEmojiClick = (emoji: Emoji) => {
const emojiAlias = ('short_name' in emoji && emoji.short_name) || emoji.name; const emojiAlias = getEmojiName(emoji);
if (!emojiAlias) { if (!emojiAlias) {
//Oops... There went something wrong //Oops... There went something wrong

Просмотреть файл

@@ -18,6 +18,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {Posts} from 'mattermost-redux/constants'; import {Posts} from 'mattermost-redux/constants';
import type {ActionResult} from 'mattermost-redux/types/actions'; import type {ActionResult} from 'mattermost-redux/types/actions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import {sortFileInfos} from 'mattermost-redux/utils/file_utils'; import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
import * as GlobalActions from 'actions/global_actions'; import * as GlobalActions from 'actions/global_actions';
@@ -1202,7 +1203,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
}; };
handleEmojiClick = (emoji: Emoji) => { handleEmojiClick = (emoji: Emoji) => {
const emojiAlias = ('short_names' in emoji && emoji.short_names && emoji.short_names[0]) || emoji.name; const emojiAlias = getEmojiName(emoji);
if (!emojiAlias) { if (!emojiAlias) {
//Oops.. There went something wrong //Oops.. There went something wrong

Просмотреть файл

@@ -4,9 +4,9 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import WithTooltip from 'components/with_tooltip';
import './multi_select_card.scss'; import './multi_select_card.scss';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
export type Props = { export type Props = {
onClick: () => void; onClick: () => void;
@@ -49,21 +49,13 @@ const MultiSelectCard = (props: Props) => {
if (props.tooltip) { if (props.tooltip) {
button = ( button = (
<OverlayTrigger <WithTooltip
className='hidden-xs' id={props.id}
delayShow={500}
placement='top' placement='top'
overlay={ title={props.tooltip}
<Tooltip
id={props.tooltip}
className='hidden-xs'
>
{props.tooltip}
</Tooltip>
}
> >
{button} {button}
</OverlayTrigger> </WithTooltip>
); );
} }

Просмотреть файл

@@ -6,10 +6,11 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {EmoticonPlusOutlineIcon} from '@mattermost/compass-icons/components'; import {EmoticonPlusOutlineIcon} from '@mattermost/compass-icons/components';
import type {Emoji, SystemEmoji} from '@mattermost/types/emojis'; import type {Emoji} from '@mattermost/types/emojis';
import type {Post} from '@mattermost/types/posts'; import type {Post} from '@mattermost/types/posts';
import type {ActionResult} from 'mattermost-redux/types/actions'; import type {ActionResult} from 'mattermost-redux/types/actions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import DeletePostModal from 'components/delete_post_modal'; import DeletePostModal from 'components/delete_post_modal';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay'; import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
@@ -390,8 +391,11 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
}; };
const handleEmojiClick = (emoji?: Emoji) => { const handleEmojiClick = (emoji?: Emoji) => {
const emojiAlias = emoji && (((emoji as SystemEmoji).short_names && (emoji as SystemEmoji).short_names[0]) || emoji.name); if (!emoji) {
return;
}
const emojiAlias = getEmojiName(emoji);
if (!emojiAlias) { if (!emojiAlias) {
//Oops.. There went something wrong //Oops.. There went something wrong
return; return;

Просмотреть файл

@@ -9,7 +9,7 @@ import type InfiniteLoader from 'react-window-infinite-loader';
import type {Emoji, EmojiCategory} from '@mattermost/types/emojis'; import type {Emoji, EmojiCategory} from '@mattermost/types/emojis';
import {isSystemEmoji} from 'mattermost-redux/utils/emoji_utils'; import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import EmojiPickerCategories from 'components/emoji_picker/components/emoji_picker_categories'; import EmojiPickerCategories from 'components/emoji_picker/components/emoji_picker_categories';
import EmojiPickerCurrentResults from 'components/emoji_picker/components/emoji_picker_current_results'; import EmojiPickerCurrentResults from 'components/emoji_picker/components/emoji_picker_current_results';
@@ -358,7 +358,7 @@ const EmojiPicker = ({
return ''; return '';
} }
const name = isSystemEmoji(emoji) ? emoji.short_name : emoji.name; const name = getEmojiName(emoji);
return name.replace(/_/g, ' '); return name.replace(/_/g, ' ');
}, [cursor.emojiId]); }, [cursor.emojiId]);

Просмотреть файл

@@ -1,64 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/shortcuts/KeyboardShortcutsSequence should create sequence with order 1`] = `
<Memo(KeyboardShortcutSequence)
hideDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Ctrl|Alt|{order}",
"id": "team.button.tooltip",
},
"mac": Object {
"defaultMessage": "⌘|⌥|{order}",
"id": "team.button.tooltip.mac",
},
}
}
values={
Object {
"order": 3,
}
}
>
<div
className="shortcut-line"
>
<ShortcutKey
key="Ctrl"
variant="tooltip"
>
<mark
className="shortcut-key shortcut-key--tooltip"
>
Ctrl
</mark>
</ShortcutKey>
<ShortcutKey
key="Alt"
variant="tooltip"
>
<mark
className="shortcut-key shortcut-key--tooltip"
>
Alt
</mark>
</ShortcutKey>
<ShortcutKey
key="3"
variant="tooltip"
>
<mark
className="shortcut-key shortcut-key--tooltip"
>
3
</mark>
</ShortcutKey>
</div>
</Memo(KeyboardShortcutSequence)>
`;
exports[`components/shortcuts/KeyboardShortcutsSequence should match snapshot when used for modal with description 1`] = ` exports[`components/shortcuts/KeyboardShortcutsSequence should match snapshot when used for modal with description 1`] = `
<Memo(KeyboardShortcutSequence) <Memo(KeyboardShortcutSequence)
shortcut={ shortcut={

Просмотреть файл

@@ -9,12 +9,6 @@ export type KeyboardShortcutDescriptor =
| MessageDescriptor | MessageDescriptor
| {default: MessageDescriptor; mac?: MessageDescriptor}; | {default: MessageDescriptor; mac?: MessageDescriptor};
export function isMessageDescriptor(
descriptor: KeyboardShortcutDescriptor,
): descriptor is MessageDescriptor {
return Boolean((descriptor as MessageDescriptor).id);
}
const callsKBShortcuts = { const callsKBShortcuts = {
global: { global: {
callsJoinCall: { callsJoinCall: {
@@ -171,16 +165,6 @@ export const KEYBOARD_SHORTCUTS = {
defaultMessage: 'Navigate to a specific team:\t⌘|⌥|[1-9]', defaultMessage: 'Navigate to a specific team:\t⌘|⌥|[1-9]',
}, },
}, },
teamNavigation: {
default: {
id: t('team.button.tooltip'),
defaultMessage: 'Ctrl|Alt|{order}',
},
mac: {
id: t('team.button.tooltip.mac'),
defaultMessage: '⌘|⌥|{order}',
},
},
navSwitcher: { navSwitcher: {
default: { default: {
id: t('shortcuts.nav.switcher'), id: t('shortcuts.nav.switcher'),

Просмотреть файл

@@ -7,8 +7,6 @@ import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import KeyboardShortcutsSequence from './keyboard_shortcuts_sequence'; import KeyboardShortcutsSequence from './keyboard_shortcuts_sequence';
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from './index';
describe('components/shortcuts/KeyboardShortcutsSequence', () => { describe('components/shortcuts/KeyboardShortcutsSequence', () => {
test('should match snapshot when used for modal with description', () => { test('should match snapshot when used for modal with description', () => {
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
@@ -95,21 +93,4 @@ describe('components/shortcuts/KeyboardShortcutsSequence', () => {
expect(wrapper.find('.shortcut-key--tooltip')).toHaveLength(2); expect(wrapper.find('.shortcut-key--tooltip')).toHaveLength(2);
expect(wrapper.find('.shortcut-key--shortcut-modal')).toHaveLength(0); expect(wrapper.find('.shortcut-key--shortcut-modal')).toHaveLength(0);
}); });
test('should create sequence with order', () => {
const order = 3;
const wrapper = mountWithIntl(
<KeyboardShortcutSequence
shortcut={KEYBOARD_SHORTCUTS.teamNavigation}
values={{order}}
hideDescription={true}
isInsideTooltip={true}
/>,
);
expect(wrapper).toMatchSnapshot();
const tag = <span>{'Keyboard shortcuts'}</span>;
expect(wrapper.contains(tag)).toEqual(false);
expect(wrapper.find('.shortcut-key--tooltip')).toHaveLength(3);
expect(wrapper.find('.shortcut-key--shortcut-modal')).toHaveLength(0);
});
}); });

Просмотреть файл

@@ -1,22 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type {FormatXMLElementFn, PrimitiveType} from 'intl-messageformat';
import React, {memo} from 'react'; import React, {memo} from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {ShortcutKeyVariant, ShortcutKey} from 'components/shortcut_key'; import {ShortcutKeyVariant, ShortcutKey} from 'components/shortcut_key';
import {isMessageDescriptor} from 'utils/i18n';
import {isMac} from 'utils/user_agent'; import {isMac} from 'utils/user_agent';
import {isMessageDescriptor} from './keyboard_shortcuts';
import type {KeyboardShortcutDescriptor} from './keyboard_shortcuts'; import type {KeyboardShortcutDescriptor} from './keyboard_shortcuts';
import './keyboard_shortcuts_sequence.scss'; import './keyboard_shortcuts_sequence.scss';
type Props = { type Props = {
shortcut: KeyboardShortcutDescriptor; shortcut: KeyboardShortcutDescriptor;
values?: Record<string, PrimitiveType | FormatXMLElementFn<string, string>>;
hideDescription?: boolean; hideDescription?: boolean;
hoistDescription?: boolean; hoistDescription?: boolean;
isInsideTooltip?: boolean; isInsideTooltip?: boolean;
@@ -32,9 +30,9 @@ function normalizeShortcutDescriptor(shortcut: KeyboardShortcutDescriptor) {
const KEY_SEPARATOR = '|'; const KEY_SEPARATOR = '|';
function KeyboardShortcutSequence({shortcut, values, hideDescription, hoistDescription, isInsideTooltip}: Props) { function KeyboardShortcutSequence({shortcut, hideDescription, hoistDescription, isInsideTooltip}: Props) {
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const shortcutText = formatMessage(normalizeShortcutDescriptor(shortcut), values); const shortcutText = formatMessage(normalizeShortcutDescriptor(shortcut));
const splitShortcut = shortcutText.split('\t'); const splitShortcut = shortcutText.split('\t');
let description = ''; let description = '';

Просмотреть файл

@@ -17,27 +17,14 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
target={[MockFunction]} target={[MockFunction]}
topOffset={-7} topOffset={-7}
/> />
<OverlayTrigger <WithTooltip
className="hidden-xs" id="reaction-icon-tooltip"
defaultOverlayShown={false}
delayShow={500}
overlay={
<Tooltip
className="hidden-xs"
id="reaction-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Add Reaction"
id="post_info.tooltip.add_reactions"
/>
</Tooltip>
}
placement="top" placement="top"
trigger={ title={
Array [ Object {
"hover", "defaultMessage": "Add Reaction",
"focus", "id": "post_info.tooltip.add_reactions",
] }
} }
> >
<button <button
@@ -51,6 +38,6 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
className="icon icon--small" className="icon icon--small"
/> />
</button> </button>
</OverlayTrigger> </WithTooltip>
</Connect(Component)> </Connect(Component)>
`; `;

Просмотреть файл

@@ -4,9 +4,9 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import type {Emoji} from '@mattermost/types/emojis'; import {TestHelper} from 'utils/test_helper';
import PostReaction from 'components/post_view/post_reaction/post_reaction'; import PostReaction from './post_reaction';
describe('components/post_view/PostReaction', () => { describe('components/post_view/PostReaction', () => {
const baseProps = { const baseProps = {
@@ -31,7 +31,7 @@ describe('components/post_view/PostReaction', () => {
const wrapper = shallow(<PostReaction {...baseProps}/>); const wrapper = shallow(<PostReaction {...baseProps}/>);
const instance = wrapper.instance() as PostReaction; const instance = wrapper.instance() as PostReaction;
instance.handleToggleEmoji({name: 'smile'} as Emoji); instance.handleToggleEmoji(TestHelper.getCustomEmojiMock({name: 'smile'}));
expect(baseProps.actions.toggleReaction).toHaveBeenCalledTimes(1); expect(baseProps.actions.toggleReaction).toHaveBeenCalledTimes(1);
expect(baseProps.actions.toggleReaction).toHaveBeenCalledWith('post_id_1', 'smile'); expect(baseProps.actions.toggleReaction).toHaveBeenCalledWith('post_id_1', 'smile');
expect(baseProps.toggleEmojiPicker).toHaveBeenCalledTimes(1); expect(baseProps.toggleEmojiPicker).toHaveBeenCalledTimes(1);

Просмотреть файл

@@ -3,23 +3,30 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {defineMessages} from 'react-intl';
import type {Emoji} from '@mattermost/types/emojis'; import type {Emoji} from '@mattermost/types/emojis';
import Permissions from 'mattermost-redux/constants/permissions'; import Permissions from 'mattermost-redux/constants/permissions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay'; import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
import OverlayTrigger from 'components/overlay_trigger';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import Tooltip from 'components/tooltip';
import EmojiIcon from 'components/widgets/icons/emoji_icon'; import EmojiIcon from 'components/widgets/icons/emoji_icon';
import WithTooltip from 'components/with_tooltip';
import {Locations} from 'utils/constants'; import {Locations} from 'utils/constants';
import {localizeMessage} from 'utils/utils'; import {localizeMessage} from 'utils/utils';
const TOP_OFFSET = -7; const TOP_OFFSET = -7;
const messages = defineMessages({
addReaction: {
id: 'post_info.tooltip.add_reactions',
defaultMessage: 'Add Reaction',
},
});
export type Props = { export type Props = {
channelId?: string; channelId?: string;
postId: string; postId: string;
@@ -46,7 +53,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
handleToggleEmoji = (emoji: Emoji): void => { handleToggleEmoji = (emoji: Emoji): void => {
this.setState({showEmojiPicker: false}); this.setState({showEmojiPicker: false});
const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name; const emojiName = getEmojiName(emoji);
this.props.actions.toggleReaction(this.props.postId, emojiName); this.props.actions.toggleReaction(this.props.postId, emojiName);
this.props.toggleEmojiPicker(); this.props.toggleEmojiPicker();
}; };
@@ -83,21 +90,10 @@ export default class PostReaction extends React.PureComponent<Props, State> {
spaceRequiredAbove={spaceRequiredAbove} spaceRequiredAbove={spaceRequiredAbove}
spaceRequiredBelow={spaceRequiredBelow} spaceRequiredBelow={spaceRequiredBelow}
/> />
<OverlayTrigger <WithTooltip
className='hidden-xs' id='reaction-icon-tooltip'
delayShow={500} title={messages.addReaction}
placement='top' placement='top'
overlay={
<Tooltip
id='reaction-icon-tooltip'
className='hidden-xs'
>
<FormattedMessage
id='post_info.tooltip.add_reactions'
defaultMessage='Add Reaction'
/>
</Tooltip>
}
> >
<button <button
data-testid='post-reaction-emoji-icon' data-testid='post-reaction-emoji-icon'
@@ -110,7 +106,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
> >
<EmojiIcon className='icon icon--small'/> <EmojiIcon className='icon icon--small'/>
</button> </button>
</OverlayTrigger> </WithTooltip>
</React.Fragment> </React.Fragment>
</ChannelPermissionGate> </ChannelPermissionGate>
); );

Просмотреть файл

@@ -6,11 +6,10 @@ import React from 'react';
import type {Emoji} from '@mattermost/types/emojis'; import type {Emoji} from '@mattermost/types/emojis';
import Permissions from 'mattermost-redux/constants/permissions'; import Permissions from 'mattermost-redux/constants/permissions';
import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils'; import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import OverlayTrigger from 'components/overlay_trigger';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import Tooltip from 'components/tooltip'; import WithTooltip from 'components/with_tooltip';
import {Locations} from 'utils/constants'; import {Locations} from 'utils/constants';
@@ -43,7 +42,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
}; };
handleToggleEmoji = (emoji: Emoji): void => { handleToggleEmoji = (emoji: Emoji): void => {
const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name; const emojiName = getEmojiName(emoji);
this.props.actions.toggleReaction(this.props.postId, emojiName); this.props.actions.toggleReaction(this.props.postId, emojiName);
}; };
@@ -70,7 +69,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
function capitalizeFirstLetter(s: string) { function capitalizeFirstLetter(s: string) {
return s[0].toLocaleUpperCase(locale) + s.slice(1); return s[0].toLocaleUpperCase(locale) + s.slice(1);
} }
const name = 'short_name' in emoji ? emoji.short_name : emoji.name; const name = getEmojiName(emoji);
return capitalizeFirstLetter(name.replace(/_/g, ' ')); return capitalizeFirstLetter(name.replace(/_/g, ' '));
}; };
@@ -92,25 +91,12 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
teamId={teamId} teamId={teamId}
permissions={[Permissions.ADD_REACTION]} permissions={[Permissions.ADD_REACTION]}
> >
<OverlayTrigger <WithTooltip
className='hidden-xs' id='post_info.emoji.tooltip'
delayShow={500} title={this.emojiName(emoji, this.props.locale)}
emoji={getEmojiName(emoji)}
emojiStyle='large'
placement='top' placement='top'
overlay={
<Tooltip
id='post_info.emoji.tooltip'
className='hidden-xs'
>
<div>
<img
className='Reaction__emoji Reaction__emoji--large'
src={getEmojiImageUrl(emoji)}
width={48}
/>
</div>
{this.emojiName(emoji, this.props.locale)}
</Tooltip>
}
> >
<div> <div>
<React.Fragment> <React.Fragment>
@@ -121,7 +107,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
/> />
</React.Fragment> </React.Fragment>
</div> </div>
</OverlayTrigger> </WithTooltip>
</ChannelPermissionGate> </ChannelPermissionGate>
), ),
); );

Просмотреть файл

@@ -7,7 +7,7 @@ import {useIntl} from 'react-intl';
import type {Emoji} from '@mattermost/types/emojis'; import type {Emoji} from '@mattermost/types/emojis';
import {getEmojiImageUrl, isSystemEmoji} from 'mattermost-redux/utils/emoji_utils'; import {getEmojiImageUrl, getEmojiName} from 'mattermost-redux/utils/emoji_utils';
type Props = { type Props = {
emoji: Emoji; emoji: Emoji;
@@ -24,7 +24,7 @@ const EmojiItem = ({emoji, onItemClick, order}: Props) => {
const itemClassName = 'post-menu__item'; const itemClassName = 'post-menu__item';
const emojiName = isSystemEmoji(emoji) ? emoji.short_name ?? emoji.name : emoji.name; const emojiName = getEmojiName(emoji);
return ( return (
<div <div

Просмотреть файл

@@ -1,50 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/post_view/Reaction should apply read-only class if user does not have permission to add reaction 1`] = ` exports[`components/post_view/Reaction should apply read-only class if user does not have permission to add reaction 1`] = `
<OverlayTrigger <Connect(ReactionTooltip)
defaultOverlayShown={false} canAddReactions={false}
delayShow={500} canRemoveReactions={true}
onEnter={[Function]} currentUserReacted={false}
overlay={ emojiName="smile"
<Tooltip id="post_id-smile-reaction"
id="post_id-smile-reaction" onShow={[Function]}
> reactions={
<Memo(Connect(ReactionTooltip))
canAddReactions={false}
canRemoveReactions={true}
currentUserReacted={false}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
Array [ Array [
"hover", Object {
"focus", "create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
] ]
} }
> >
@@ -87,54 +64,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does
</span> </span>
</span> </span>
</button> </button>
</OverlayTrigger> </Connect(ReactionTooltip)>
`; `;
exports[`components/post_view/Reaction should apply read-only class if user does not have permission to remove reaction 1`] = ` exports[`components/post_view/Reaction should apply read-only class if user does not have permission to remove reaction 1`] = `
<OverlayTrigger <Connect(ReactionTooltip)
defaultOverlayShown={false} canAddReactions={true}
delayShow={500} canRemoveReactions={false}
onEnter={[Function]} currentUserReacted={true}
overlay={ emojiName="smile"
<Tooltip id="post_id-smile-reaction"
id="post_id-smile-reaction" onShow={[Function]}
> reactions={
<Memo(Connect(ReactionTooltip))
canAddReactions={true}
canRemoveReactions={false}
currentUserReacted={true}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
Array [ Array [
"hover", Object {
"focus", "create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
] ]
} }
> >
@@ -177,54 +131,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does
</span> </span>
</span> </span>
</button> </button>
</OverlayTrigger> </Connect(ReactionTooltip)>
`; `;
exports[`components/post_view/Reaction should match snapshot 1`] = ` exports[`components/post_view/Reaction should match snapshot 1`] = `
<OverlayTrigger <Connect(ReactionTooltip)
defaultOverlayShown={false} canAddReactions={true}
delayShow={500} canRemoveReactions={true}
onEnter={[Function]} currentUserReacted={false}
overlay={ emojiName="smile"
<Tooltip id="post_id-smile-reaction"
id="post_id-smile-reaction" onShow={[Function]}
> reactions={
<Memo(Connect(ReactionTooltip))
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={false}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
Array [ Array [
"hover", Object {
"focus", "create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
] ]
} }
> >
@@ -267,54 +198,31 @@ exports[`components/post_view/Reaction should match snapshot 1`] = `
</span> </span>
</span> </span>
</button> </button>
</OverlayTrigger> </Connect(ReactionTooltip)>
`; `;
exports[`components/post_view/Reaction should match snapshot when a current user reacted to a post 1`] = ` exports[`components/post_view/Reaction should match snapshot when a current user reacted to a post 1`] = `
<OverlayTrigger <Connect(ReactionTooltip)
defaultOverlayShown={false} canAddReactions={true}
delayShow={500} canRemoveReactions={true}
onEnter={[Function]} currentUserReacted={true}
overlay={ emojiName="smile"
<Tooltip id="post_id-smile-reaction"
id="post_id-smile-reaction" onShow={[Function]}
> reactions={
<Memo(Connect(ReactionTooltip))
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={true}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":cry:",
"post_id": "post_id",
"user_id": "user_id_1",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
Array [ Array [
"hover", Object {
"focus", "create_at": 0,
"emoji_name": ":cry:",
"post_id": "post_id",
"user_id": "user_id_1",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
] ]
} }
> >
@@ -357,7 +265,7 @@ exports[`components/post_view/Reaction should match snapshot when a current user
</span> </span>
</span> </span>
</button> </button>
</OverlayTrigger> </Connect(ReactionTooltip)>
`; `;
exports[`components/post_view/Reaction should return null/empty if no emojiImageUrl 1`] = `""`; exports[`components/post_view/Reaction should return null/empty if no emojiImageUrl 1`] = `""`;

Просмотреть файл

@@ -71,14 +71,6 @@
margin: 0 2px 0 0; margin: 0 2px 0 0;
object-fit: contain; object-fit: contain;
vertical-align: middle; vertical-align: middle;
&--large {
width: 48px;
max-width: none;
height: 48px;
max-height: none;
margin: 4px 0;
}
} }
&__emoji--post-menu { &__emoji--post-menu {

Просмотреть файл

@@ -6,9 +6,6 @@ import React from 'react';
import type {Post} from '@mattermost/types/posts'; import type {Post} from '@mattermost/types/posts';
import type {Reaction as ReactionType} from '@mattermost/types/reactions'; import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
import ReactionTooltip from './reaction_tooltip'; import ReactionTooltip from './reaction_tooltip';
@@ -218,23 +215,14 @@ export default class Reaction extends React.PureComponent<Props, State> {
); );
return ( return (
<OverlayTrigger <ReactionTooltip
delayShow={500} id={`${this.props.post.id}-${this.props.emojiName}-reaction`}
placement='top' canAddReactions={canAddReactions}
shouldUpdatePosition={true} canRemoveReactions={canRemoveReactions}
overlay={ currentUserReacted={currentUserReacted}
<Tooltip id={`${this.props.post.id}-${this.props.emojiName}-reaction`}> emojiName={emojiName}
<ReactionTooltip reactions={reactions}
canAddReactions={canAddReactions} onShow={this.loadMissingProfiles}
canRemoveReactions={canRemoveReactions}
currentUserReacted={currentUserReacted}
emojiName={emojiName}
emojiIcon={emojiIcon}
reactions={reactions}
/>
</Tooltip>
}
onEnter={this.loadMissingProfiles}
> >
<button <button
id={`postReaction-${this.props.post.id}-${this.props.emojiName}`} id={`postReaction-${this.props.post.id}-${this.props.emojiName}`}
@@ -262,7 +250,7 @@ export default class Reaction extends React.PureComponent<Props, State> {
</span> </span>
</span> </span>
</button> </button>
</OverlayTrigger> </ReactionTooltip>
); );
} }
} }

Просмотреть файл

@@ -2,16 +2,20 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {useIntl} from 'react-intl';
import type {Reaction as ReactionType} from '@mattermost/types/reactions'; import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import WithTooltip from 'components/with_tooltip';
type Props = { type Props = {
canAddReactions: boolean; canAddReactions: boolean;
canRemoveReactions: boolean; canRemoveReactions: boolean;
children: React.ReactNode;
currentUserReacted: boolean; currentUserReacted: boolean;
emojiName: string; emojiName: string;
emojiIcon: React.ReactNode; id: string;
onShow: () => void;
reactions: ReactionType[]; reactions: ReactionType[];
users: string[]; users: string[];
}; };
@@ -20,123 +24,120 @@ const ReactionTooltip: React.FC<Props> = (props: Props) => {
const { const {
canAddReactions, canAddReactions,
canRemoveReactions, canRemoveReactions,
children,
currentUserReacted, currentUserReacted,
emojiIcon,
emojiName, emojiName,
id,
onShow,
reactions, reactions,
users, users,
} = props; } = props;
const intl = useIntl();
const otherUsersCount = reactions.length - users.length; const otherUsersCount = reactions.length - users.length;
let names: React.ReactNode; let names;
if (otherUsersCount > 0) { if (otherUsersCount > 0) {
if (users.length > 0) { if (users.length > 0) {
names = ( names = intl.formatMessage(
<FormattedMessage {
id='reaction.usersAndOthersReacted' id: 'reaction.usersAndOthersReacted',
defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}' defaultMessage: '{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}',
values={{ },
users: users.join(', '), {
otherUsers: otherUsersCount, users: users.join(', '),
}} otherUsers: otherUsersCount,
/> },
); );
} else { } else {
names = ( names = intl.formatMessage(
<FormattedMessage {
id='reaction.othersReacted' id: 'reaction.othersReacted',
defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}' defaultMessage: '{otherUsers, number} {otherUsers, plural, one {user} other {users}}',
values={{ },
otherUsers: otherUsersCount, {
}} otherUsers: otherUsersCount,
/> },
); );
} }
} else if (users.length > 1) { } else if (users.length > 1) {
names = ( names = intl.formatMessage(
<FormattedMessage {
id='reaction.usersReacted' id: 'reaction.usersReacted',
defaultMessage='{users} and {lastUser}' defaultMessage: '{users} and {lastUser}',
values={{ },
users: users.slice(0, -1).join(', '), {
lastUser: users[users.length - 1], users: users.slice(0, -1).join(', '),
}} lastUser: users[users.length - 1],
/> },
); );
} else { } else {
names = users[0]; names = users[0];
} }
let reactionVerb: React.ReactNode; let reactionVerb;
if (users.length + otherUsersCount > 1) { if (users.length + otherUsersCount > 1) {
if (currentUserReacted) { if (currentUserReacted) {
reactionVerb = ( reactionVerb = intl.formatMessage({
<FormattedMessage id: 'reaction.reactionVerb.youAndUsers',
id='reaction.reactionVerb.youAndUsers' defaultMessage: 'reacted',
defaultMessage='reacted' });
/>
);
} else { } else {
reactionVerb = ( reactionVerb = intl.formatMessage({
<FormattedMessage id: 'reaction.reactionVerb.users',
id='reaction.reactionVerb.users' defaultMessage: 'reacted',
defaultMessage='reacted' });
/>
);
} }
} else if (currentUserReacted) { } else if (currentUserReacted) {
reactionVerb = ( reactionVerb = intl.formatMessage({
<FormattedMessage id: 'reaction.reactionVerb.you',
id='reaction.reactionVerb.you' defaultMessage: 'reacted',
defaultMessage='reacted' });
/>
);
} else { } else {
reactionVerb = ( reactionVerb = intl.formatMessage({
<FormattedMessage id: 'reaction.reactionVerb.user',
id='reaction.reactionVerb.user' defaultMessage: 'reacted',
defaultMessage='reacted' });
/>
);
} }
const tooltip = ( const tooltip = intl.formatMessage(
<FormattedMessage {
id='reaction.reacted' id: 'reaction.reacted',
defaultMessage='{users} {reactionVerb} with {emoji}' defaultMessage: '{users} {reactionVerb} with {emoji}',
values={{ },
users: <b>{names}</b>, {
reactionVerb, users: names,
emoji: <b>{':' + emojiName + ':'}</b>, reactionVerb,
}} emoji: ':' + emojiName + ':',
/> },
); );
let clickTooltip: React.ReactNode; let clickTooltip;
if (currentUserReacted && canRemoveReactions) { if (currentUserReacted && canRemoveReactions) {
clickTooltip = ( clickTooltip = intl.formatMessage({
<FormattedMessage id: 'reaction.clickToRemove',
id='reaction.clickToRemove' defaultMessage: '(click to remove)',
defaultMessage='(click to remove)' });
/>
);
} else if (!currentUserReacted && canAddReactions) { } else if (!currentUserReacted && canAddReactions) {
clickTooltip = ( clickTooltip = intl.formatMessage({
<FormattedMessage id: 'reaction.clickToAdd',
id='reaction.clickToAdd' defaultMessage: '(click to add)',
defaultMessage='(click to add)' });
/>
);
} }
return ( return (
<> <WithTooltip
<div className='reaction-emoji--large'>{emojiIcon}</div> id={id}
{tooltip} emoji={emojiName}
<br/> emojiStyle='large'
{clickTooltip} placement='top'
</> title={tooltip}
hint={clickTooltip}
onShow={onShow}
>
{children}
</WithTooltip>
); );
}; };

Просмотреть файл

@@ -71,25 +71,14 @@ exports[`components/ReactionList should render when there are reactions 1`] = `
} }
teamId="teamId" teamId="teamId"
> >
<OverlayTrigger <WithTooltip
defaultOverlayShown={false} id="addReactionTooltip"
delayShow={400}
overlay={
<Tooltip
id="addReactionTooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Add a reaction"
id="reaction_list.addReactionTooltip"
/>
</Tooltip>
}
placement="top" placement="top"
trigger={ title={
Array [ Object {
"hover", "defaultMessage": "Add a reaction",
"focus", "id": "reaction_list.addReactionTooltip",
] }
} }
> >
<button <button
@@ -104,7 +93,7 @@ exports[`components/ReactionList should render when there are reactions 1`] = `
<AddReactionIcon /> <AddReactionIcon />
</span> </span>
</button> </button>
</OverlayTrigger> </WithTooltip>
</Connect(Component)> </Connect(Component)>
</span> </span>
</div> </div>

Просмотреть файл

@@ -2,28 +2,33 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {defineMessages} from 'react-intl';
import type {Emoji} from '@mattermost/types/emojis'; import type {Emoji} from '@mattermost/types/emojis';
import type {Post} from '@mattermost/types/posts'; import type {Post} from '@mattermost/types/posts';
import type {Reaction as ReactionType} from '@mattermost/types/reactions'; import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import Permissions from 'mattermost-redux/constants/permissions'; import Permissions from 'mattermost-redux/constants/permissions';
import {isSystemEmoji} from 'mattermost-redux/utils/emoji_utils'; import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay'; import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
import OverlayTrigger from 'components/overlay_trigger';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import Reaction from 'components/post_view/reaction'; import Reaction from 'components/post_view/reaction';
import Tooltip from 'components/tooltip';
import AddReactionIcon from 'components/widgets/icons/add_reaction_icon'; import AddReactionIcon from 'components/widgets/icons/add_reaction_icon';
import WithTooltip from 'components/with_tooltip';
import Constants from 'utils/constants';
import {localizeMessage} from 'utils/utils'; import {localizeMessage} from 'utils/utils';
const DEFAULT_EMOJI_PICKER_RIGHT_OFFSET = 15; const DEFAULT_EMOJI_PICKER_RIGHT_OFFSET = 15;
const EMOJI_PICKER_WIDTH_OFFSET = 260; const EMOJI_PICKER_WIDTH_OFFSET = 260;
const messages = defineMessages({
addAReaction: {
id: 'reaction_list.addReactionTooltip',
defaultMessage: 'Add a reaction',
},
});
type Props = { type Props = {
/** /**
@@ -90,7 +95,7 @@ export default class ReactionList extends React.PureComponent<Props, State> {
handleEmojiClick = (emoji: Emoji): void => { handleEmojiClick = (emoji: Emoji): void => {
this.setState({showEmojiPicker: false}); this.setState({showEmojiPicker: false});
const emojiName = isSystemEmoji(emoji) ? emoji.short_names[0] : emoji.name; const emojiName = getEmojiName(emoji);
this.props.actions.toggleReaction(this.props.post.id, emojiName); this.props.actions.toggleReaction(this.props.post.id, emojiName);
}; };
@@ -148,15 +153,6 @@ export default class ReactionList extends React.PureComponent<Props, State> {
let emojiPicker = null; let emojiPicker = null;
if (this.props.canAddReactions) { if (this.props.canAddReactions) {
const addReactionTooltip = (
<Tooltip id='addReactionTooltip'>
<FormattedMessage
id='reaction_list.addReactionTooltip'
defaultMessage='Add a reaction'
/>
</Tooltip>
);
emojiPicker = ( emojiPicker = (
<span className='emoji-picker__container'> <span className='emoji-picker__container'>
<EmojiPickerOverlay <EmojiPickerOverlay
@@ -172,10 +168,10 @@ export default class ReactionList extends React.PureComponent<Props, State> {
teamId={this.props.teamId} teamId={this.props.teamId}
permissions={[Permissions.ADD_REACTION]} permissions={[Permissions.ADD_REACTION]}
> >
<OverlayTrigger <WithTooltip
id='addReactionTooltip'
title={messages.addAReaction}
placement='top' placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
overlay={addReactionTooltip}
> >
<button <button
aria-label={localizeMessage('reaction.add.ariaLabel', 'Add a reaction')} aria-label={localizeMessage('reaction.add.ariaLabel', 'Add a reaction')}
@@ -190,7 +186,7 @@ export default class ReactionList extends React.PureComponent<Props, State> {
<AddReactionIcon/> <AddReactionIcon/>
</span> </span>
</button> </button>
</OverlayTrigger> </WithTooltip>
</ChannelPermissionGate> </ChannelPermissionGate>
</span> </span>
); );

Просмотреть файл

@@ -545,16 +545,3 @@
right: 12px; right: 12px;
} }
} }
.tooltipTitle {
font-weight: 700;
}
.tooltipText {
color: #9a9a9a !important;
text-align: left;
}
.proratedTooltip > .tooltip-inner {
min-width: 300px;
}

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {FormattedMessage, FormattedNumber, FormattedDate} from 'react-intl'; import {FormattedMessage, FormattedNumber, FormattedDate, defineMessages} from 'react-intl';
import {useDispatch} from 'react-redux'; import {useDispatch} from 'react-redux';
import type {Invoice, InvoiceLineItem, Product} from '@mattermost/types/cloud'; import type {Invoice, InvoiceLineItem, Product} from '@mattermost/types/cloud';
@@ -14,16 +14,26 @@ import {openModal} from 'actions/views/modals';
import {getPaymentStatus} from 'components/admin_console/billing/billing_summary/billing_summary'; import {getPaymentStatus} from 'components/admin_console/billing/billing_summary/billing_summary';
import CloudInvoicePreview from 'components/cloud_invoice_preview'; import CloudInvoicePreview from 'components/cloud_invoice_preview';
import OverlayTrigger from 'components/overlay_trigger';
import type {Seats} from 'components/seats_calculator'; import type {Seats} from 'components/seats_calculator';
import SeatsCalculator from 'components/seats_calculator'; import SeatsCalculator from 'components/seats_calculator';
import Tooltip from 'components/tooltip';
import EllipsisHorizontalIcon from 'components/widgets/icons/ellipsis_h_icon'; import EllipsisHorizontalIcon from 'components/widgets/icons/ellipsis_h_icon';
import WithTooltip from 'components/with_tooltip';
import {BillingSchemes, ModalIdentifiers, TELEMETRY_CATEGORIES, CloudLinks} from 'utils/constants'; import {BillingSchemes, ModalIdentifiers, TELEMETRY_CATEGORIES, CloudLinks} from 'utils/constants';
import './renewal_card.scss'; import './renewal_card.scss';
const messages = defineMessages({
partialChargesTooltipTitle: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges',
defaultMessage: 'What are partial charges?',
},
partialChargesTooltipText: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message',
defaultMessage: 'Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.',
},
});
type RenewalCardProps = { type RenewalCardProps = {
invoice: Invoice; invoice: Invoice;
product?: Product; product?: Product;
@@ -39,6 +49,7 @@ type RenewalCardProps = {
export default function RenewalCard({invoice, product, hasMore, fullCharges, partialCharges, seats, onSeatChange, existingUsers, buttonDisabled, onButtonClick}: RenewalCardProps) { export default function RenewalCard({invoice, product, hasMore, fullCharges, partialCharges, seats, onSeatChange, existingUsers, buttonDisabled, onButtonClick}: RenewalCardProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const openInvoicePreview = () => { const openInvoicePreview = () => {
dispatch( dispatch(
openModal({ openModal({
@@ -178,32 +189,14 @@ export default function RenewalCard({invoice, product, hasMore, fullCharges, par
id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges' id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges'
defaultMessage='Partial charges' defaultMessage='Partial charges'
/> />
<OverlayTrigger <WithTooltip
delayShow={500} id='BillingSubscriptions__seatOverageTooltip'
title={messages.partialChargesTooltipTitle}
hint={messages.partialChargesTooltipText}
placement='bottom' placement='bottom'
overlay={
<Tooltip
id='BillingSubscriptions__seatOverageTooltip'
className='BillingSubscriptions__tooltip BillingSubscriptions__tooltip-right'
positionLeft={390}
>
<div className='BillingSubscriptions__tooltipTitle'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges'
defaultMessage='What are partial charges?'
/>
</div>
<div className='BillingSubscriptions__tooltipMessage'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message'
defaultMessage='Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.'
/>
</div>
</Tooltip>
}
> >
<i className='icon-information-outline'/> <i className='icon-information-outline'/>
</OverlayTrigger> </WithTooltip>
</div> </div>
{partialCharges.map((charge: any) => ( {partialCharges.map((charge: any) => (
<div <div

Просмотреть файл

@@ -2,18 +2,28 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useEffect} from 'react'; import React, {useEffect} from 'react';
import {useIntl, FormattedMessage, FormattedNumber} from 'react-intl'; import {useIntl, FormattedMessage, FormattedNumber, defineMessages} from 'react-intl';
import {InformationOutlineIcon} from '@mattermost/compass-icons/components'; import {InformationOutlineIcon} from '@mattermost/compass-icons/components';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import Input from 'components/widgets/inputs/input/input'; import Input from 'components/widgets/inputs/input/input';
import WithTooltip from 'components/with_tooltip';
import {Constants, ItemStatus} from 'utils/constants'; import {ItemStatus} from 'utils/constants';
import './seats_calculator.scss'; import './seats_calculator.scss';
const messages = defineMessages({
tooltipText: {
id: 'admin.billing.subscription.userCount.tooltipTitle',
defaultMessage: 'Current User Count',
},
tooltipTitle: {
id: 'admin.billing.subscription.userCount.tooltipText',
defaultMessage: 'You must purchase at least the current number of active users.',
},
});
interface Props { interface Props {
price: number; price: number;
seats: Seats; seats: Seats;
@@ -154,25 +164,6 @@ export default function SeatsCalculator(props: Props) {
const maxSeats = calculateMaxUsers(annualPricePerSeat); const maxSeats = calculateMaxUsers(annualPricePerSeat);
const total = '$' + intl.formatNumber((parseFloat(props.seats.quantity) || 0) * annualPricePerSeat, {maximumFractionDigits: 2}); const total = '$' + intl.formatNumber((parseFloat(props.seats.quantity) || 0) * annualPricePerSeat, {maximumFractionDigits: 2});
const userCountTooltip = (
<Tooltip
id='userCount__tooltip'
className='self-hosted-user-count-tooltip'
>
<div className='tooltipTitle'>
<FormattedMessage
defaultMessage={'Current User Count'}
id={'admin.billing.subscription.userCount.tooltipTitle'}
/>
</div>
<div className='tooltipText'>
<FormattedMessage
defaultMessage={'You must purchase at least the current number of active users.'}
id={'admin.billing.subscription.userCount.tooltipText'}
/>
</div>
</Tooltip>
);
return ( return (
<div className='SeatsCalculator'> <div className='SeatsCalculator'>
@@ -198,16 +189,17 @@ export default function SeatsCalculator(props: Props) {
</div> </div>
<div className='SeatsCalculator__seats-tooltip'> <div className='SeatsCalculator__seats-tooltip'>
<div className='icon'> <div className='icon'>
<OverlayTrigger <WithTooltip
delayShow={Constants.OVERLAY_TIME_DELAY} id='userCount__tooltip'
title={messages.tooltipTitle}
hint={messages.tooltipText}
placement='right' placement='right'
overlay={userCountTooltip}
> >
<InformationOutlineIcon <InformationOutlineIcon
size={18} size={18}
color={'rgba(var(--center-channel-text-rgb), 0.75)'} color={'rgba(var(--center-channel-text-rgb), 0.75)'}
/> />
</OverlayTrigger> </WithTooltip>
</div> </div>
</div> </div>
</div> </div>
@@ -242,6 +234,5 @@ export default function SeatsCalculator(props: Props) {
)} )}
</div> </div>
</div> </div>
); );
} }

Просмотреть файл

@@ -4,39 +4,37 @@ exports[`components/sidebar/channel_filter should match snapshot 1`] = `
<div <div
className="SidebarFilters" className="SidebarFilters"
> >
<OverlayTrigger <WithTooltip
defaultOverlayShown={false} id="channel-filter-tooltip"
delayShow={500}
overlay={
<Tooltip
className="hidden-xs"
id="new-group-tooltip"
>
Filter by unread
<Memo(KeyboardShortcutSequence)
hideDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Toggle unread/all channels: Ctrl|Shift|U",
"id": "shortcuts.nav.toggle_unreads",
},
"mac": Object {
"defaultMessage": "Toggle unread/all channels: ⌘|Shift|U",
"id": "shortcuts.nav.toggle_unreads.mac",
},
}
}
/>
</Tooltip>
}
placement="right" placement="right"
trigger={ shortcut={
Array [ Object {
"hover", "default": Array [
"focus", Object {
] "defaultMessage": "Ctrl",
"id": "shortcuts.generic.ctrl",
},
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
"mac": Array [
"⌘",
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
}
}
title={
Object {
"defaultMessage": "Filter by unread",
"id": "sidebar_left.channel_filter.filterByUnread",
}
} }
> >
<a <a
@@ -49,7 +47,7 @@ exports[`components/sidebar/channel_filter should match snapshot 1`] = `
className="icon icon-filter-variant" className="icon icon-filter-variant"
/> />
</a> </a>
</OverlayTrigger> </WithTooltip>
</div> </div>
`; `;
@@ -57,39 +55,37 @@ exports[`components/sidebar/channel_filter should match snapshot if the unread f
<div <div
className="SidebarFilters" className="SidebarFilters"
> >
<OverlayTrigger <WithTooltip
defaultOverlayShown={false} id="channel-filter-tooltip"
delayShow={500}
overlay={
<Tooltip
className="hidden-xs"
id="new-group-tooltip"
>
Show all channels
<Memo(KeyboardShortcutSequence)
hideDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Toggle unread/all channels: Ctrl|Shift|U",
"id": "shortcuts.nav.toggle_unreads",
},
"mac": Object {
"defaultMessage": "Toggle unread/all channels: ⌘|Shift|U",
"id": "shortcuts.nav.toggle_unreads.mac",
},
}
}
/>
</Tooltip>
}
placement="right" placement="right"
trigger={ shortcut={
Array [ Object {
"hover", "default": Array [
"focus", Object {
] "defaultMessage": "Ctrl",
"id": "shortcuts.generic.ctrl",
},
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
"mac": Array [
"⌘",
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
}
}
title={
Object {
"defaultMessage": "Show all channels",
"id": "sidebar_left.channel_filter.showAllChannels",
}
} }
> >
<a <a
@@ -102,6 +98,6 @@ exports[`components/sidebar/channel_filter should match snapshot if the unread f
className="icon icon-filter-variant" className="icon icon-filter-variant"
/> />
</a> </a>
</OverlayTrigger> </WithTooltip>
</div> </div>
`; `;

Просмотреть файл

@@ -3,18 +3,33 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import {injectIntl} from 'react-intl'; import {defineMessages, injectIntl} from 'react-intl';
import type {IntlShape} from 'react-intl'; import type {IntlShape} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence'; import WithTooltip from 'components/with_tooltip';
import OverlayTrigger from 'components/overlay_trigger'; import {ShortcutKeys} from 'components/with_tooltip/shortcut';
import Tooltip from 'components/tooltip';
import Constants from 'utils/constants'; import Constants from 'utils/constants';
import * as Keyboard from 'utils/keyboard'; import * as Keyboard from 'utils/keyboard';
const messages = defineMessages({
disableTooltip: {
id: 'sidebar_left.channel_filter.showAllChannels',
defaultMessage: 'Show all channels',
},
enableTooltip: {
id: 'sidebar_left.channel_filter.filterByUnread',
defaultMessage: 'Filter by unread',
},
});
const shortcut = {
default: [ShortcutKeys.ctrl, ShortcutKeys.shift, 'U'],
mac: [ShortcutKeys.cmd, ShortcutKeys.shift, 'U'],
};
type Props = { type Props = {
intl: IntlShape; intl: IntlShape;
hasMultipleTeams: boolean; hasMultipleTeams: boolean;
@@ -62,34 +77,15 @@ export class ChannelFilter extends React.PureComponent<Props> {
render() { render() {
const {intl, unreadFilterEnabled, hasMultipleTeams} = this.props; const {intl, unreadFilterEnabled, hasMultipleTeams} = this.props;
let tooltipMessage = intl.formatMessage({id: 'sidebar_left.channel_filter.filterByUnread', defaultMessage: 'Filter by unread'});
if (unreadFilterEnabled) {
tooltipMessage = intl.formatMessage({id: 'sidebar_left.channel_filter.showAllChannels', defaultMessage: 'Show all channels'});
}
const unreadsAriaLabel = intl.formatMessage({id: 'sidebar_left.channel_filter.filterUnreadAria', defaultMessage: 'unreads filter'}); const unreadsAriaLabel = intl.formatMessage({id: 'sidebar_left.channel_filter.filterUnreadAria', defaultMessage: 'unreads filter'});
const tooltip = (
<Tooltip
id='new-group-tooltip'
className='hidden-xs'
>
{tooltipMessage}
<KeyboardShortcutSequence
shortcut={KEYBOARD_SHORTCUTS.navToggleUnreads}
hideDescription={true}
isInsideTooltip={true}
/>
</Tooltip>
);
return ( return (
<div className='SidebarFilters'> <div className='SidebarFilters'>
<OverlayTrigger <WithTooltip
delayShow={500} id='channel-filter-tooltip'
title={unreadFilterEnabled ? messages.disableTooltip : messages.enableTooltip}
shortcut={shortcut}
placement={hasMultipleTeams ? 'top' : 'right'} placement={hasMultipleTeams ? 'top' : 'right'}
overlay={tooltip}
> >
<a <a
href='#' href='#'
@@ -101,7 +97,7 @@ export class ChannelFilter extends React.PureComponent<Props> {
> >
<i className='icon icon-filter-variant'/> <i className='icon icon-filter-variant'/>
</a> </a>
</OverlayTrigger> </WithTooltip>
</div> </div>
); );
} }

Просмотреть файл

@@ -2,25 +2,26 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, {useCallback, useMemo} from 'react';
import {Draggable} from 'react-beautiful-dnd'; import {Draggable} from 'react-beautiful-dnd';
import {injectIntl} from 'react-intl'; import {defineMessages, useIntl} from 'react-intl';
import type {IntlShape} from 'react-intl';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {mark, trackEvent} from 'actions/telemetry_actions.jsx'; import {mark, trackEvent} from 'actions/telemetry_actions.jsx';
import CopyUrlContextMenu from 'components/copy_url_context_menu'; import CopyUrlContextMenu from 'components/copy_url_context_menu';
import KeyboardShortcutSequence, {
KEYBOARD_SHORTCUTS,
} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import TeamIcon from 'components/widgets/team_icon/team_icon'; import TeamIcon from 'components/widgets/team_icon/team_icon';
import WithTooltip from 'components/with_tooltip';
import {ShortcutKeys} from 'components/with_tooltip/shortcut';
import Constants from 'utils/constants';
import {isDesktopApp} from 'utils/user_agent'; import {isDesktopApp} from 'utils/user_agent';
import {localizeMessage} from 'utils/utils';
const messages = defineMessages({
nameUndefined: {
id: 'team.button.name_undefined',
defaultMessage: 'This team does not have a name',
},
});
interface Props { interface Props {
btnClass?: string; btnClass?: string;
@@ -36,7 +37,6 @@ interface Props {
placement?: 'left' | 'right' | 'top' | 'bottom'; placement?: 'left' | 'right' | 'top' | 'bottom';
teamIconUrl?: string | null; teamIconUrl?: string | null;
switchTeam: (url: string) => void; switchTeam: (url: string) => void;
intl: IntlShape;
isDraggable?: boolean; isDraggable?: boolean;
teamIndex?: number; teamIndex?: number;
teamId?: string; teamId?: string;
@@ -44,178 +44,201 @@ interface Props {
hasUrgent?: boolean; hasUrgent?: boolean;
} }
class TeamButton extends React.PureComponent<Props> { export default function TeamButton({
handleSwitch = (e: React.MouseEvent) => { btnClass,
url,
displayName,
order,
unread,
mentions,
teamIconUrl,
isDraggable = false,
switchTeam,
teamIndex,
teamId,
tip,
...otherProps
}: Props) {
const {formatMessage} = useIntl();
const handleSwitch = useCallback((e: React.MouseEvent) => {
mark('TeamLink#click'); mark('TeamLink#click');
e.preventDefault(); e.preventDefault();
this.props.switchTeam(this.props.url); switchTeam(url);
setTimeout(() => { setTimeout(() => {
trackEvent('ui', 'ui_team_sidebar_switch_team'); trackEvent('ui', 'ui_team_sidebar_switch_team');
}, 0); }, 0);
}; }, [switchTeam, url]);
render() { let teamClass: string = otherProps.active ? 'active' : '';
const {teamIconUrl, displayName, btnClass, mentions, unread, isDraggable = false, teamIndex, teamId, order} = this.props; const isNotCreateTeamButton: boolean = !url.endsWith('create_team') && !url.endsWith('select_team');
const {formatMessage} = this.props.intl;
let teamClass: string = this.props.active ? 'active' : ''; let badge: JSX.Element | undefined;
const isNotCreateTeamButton: boolean = !this.props.url.endsWith('create_team') && !this.props.url.endsWith('select_team');
let badge: JSX.Element | undefined; let ariaLabel = formatMessage({
id: 'team.button.ariaLabel',
defaultMessage: '{teamName} team',
},
{
teamName: displayName,
});
let ariaLabel = formatMessage({ if (!teamClass) {
id: 'team.button.ariaLabel', if (unread && !otherProps.isInProduct) {
defaultMessage: '{teamName} team', teamClass = 'unread';
badge = (
<span className={'unread-badge'}/>
);
} else if (isNotCreateTeamButton) {
teamClass = '';
} else {
teamClass = 'special';
}
ariaLabel = formatMessage({
id: 'team.button.unread.ariaLabel',
defaultMessage: '{teamName} team unread',
}, },
{ {
teamName: displayName, teamName: displayName,
}); });
if (!teamClass) { if (mentions) {
if (unread && !this.props.isInProduct) {
teamClass = 'unread';
badge = (
<span className={'unread-badge'}/>
);
} else if (isNotCreateTeamButton) {
teamClass = '';
} else {
teamClass = 'special';
}
ariaLabel = formatMessage({ ariaLabel = formatMessage({
id: 'team.button.unread.ariaLabel', id: 'team.button.mentions.ariaLabel',
defaultMessage: '{teamName} team unread', defaultMessage: '{teamName} team, {mentionCount} mentions',
}, },
{ {
teamName: displayName, teamName: displayName,
mentionCount: mentions,
}); });
if (mentions) { badge = (
ariaLabel = formatMessage({ <span className={classNames('badge badge-max-number pull-right small', {urgent: otherProps.hasUrgent})}>{mentions > 99 ? '99+' : mentions}</span>
id: 'team.button.mentions.ariaLabel',
defaultMessage: '{teamName} team, {mentionCount} mentions',
},
{
teamName: displayName,
mentionCount: mentions,
});
badge = (
<span className={classNames('badge badge-max-number pull-right small', {urgent: this.props.hasUrgent})}>{mentions > 99 ? '99+' : mentions}</span>
);
}
}
ariaLabel = ariaLabel.toLowerCase();
const content = (
<TeamIcon
className={teamClass}
withHover={true}
content={this.props.content || displayName || ''}
url={teamIconUrl}
/>
);
let toolTip = this.props.tip || localizeMessage('team.button.name_undefined', 'This team does not have a name');
let orderIndicator: JSX.Element | undefined;
if (typeof this.props.order !== 'undefined' && this.props.order < 10) {
toolTip = (
<>
{toolTip}
<KeyboardShortcutSequence
shortcut={KEYBOARD_SHORTCUTS.teamNavigation}
values={{order}}
hideDescription={true}
isInsideTooltip={true}
/>
</>
); );
}
}
if (this.props.showOrder) { ariaLabel = ariaLabel.toLowerCase();
orderIndicator = (
<div className='order-indicator'> const content = (
{order} <TeamIcon
className={teamClass}
withHover={true}
content={otherProps.content || displayName || ''}
url={teamIconUrl}
/>
);
let orderIndicator: JSX.Element | undefined;
if (typeof order !== 'undefined' && order < 10) {
if (otherProps.showOrder) {
orderIndicator = (
<div className='order-indicator'>
{order}
</div>
);
}
}
const btn = (
<WithTeamTooltip
order={order}
tip={tip}
url={url}
>
<div className={'team-btn ' + btnClass}>
{!otherProps.isInProduct && badge}
{content}
</div>
</WithTeamTooltip>
);
let teamButton = (
<Link
id={`${url.slice(1)}TeamButton`}
aria-label={ariaLabel}
to={url}
onClick={handleSwitch}
>
{btn}
</Link>
);
if (isDesktopApp()) {
// if this is not a "special" team button, give it a context menu
if (isNotCreateTeamButton) {
teamButton = (
<CopyUrlContextMenu
link={url}
menuId={url}
>
{teamButton}
</CopyUrlContextMenu>
);
}
}
return isDraggable ? (
<Draggable
draggableId={teamId!}
index={teamIndex!}
>
{(provided, snapshot) => {
return (
<div
className='draggable-team-container'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div
className={classNames([`team-container ${teamClass}`, {isDragging: snapshot.isDragging}])}
>
{teamButton}
{orderIndicator}
</div>
</div> </div>
); );
} }}
} </Draggable>
) : (
const btn = ( <div className={`team-container ${teamClass}`}>
<OverlayTrigger {teamButton}
delayShow={Constants.OVERLAY_TIME_DELAY} {orderIndicator}
placement={this.props.placement} </div>
overlay={ );
<Tooltip id={`tooltip-${this.props.url}`}>
{toolTip}
</Tooltip>
}
>
<div className={'team-btn ' + btnClass}>
{!this.props.isInProduct && badge}
{content}
</div>
</OverlayTrigger>
);
let teamButton = (
<Link
id={`${this.props.url.slice(1)}TeamButton`}
aria-label={ariaLabel}
to={this.props.url}
onClick={this.handleSwitch}
>
{btn}
</Link>
);
if (isDesktopApp()) {
// if this is not a "special" team button, give it a context menu
if (isNotCreateTeamButton) {
teamButton = (
<CopyUrlContextMenu
link={this.props.url}
menuId={this.props.url}
>
{teamButton}
</CopyUrlContextMenu>
);
}
}
return isDraggable ? (
<Draggable
draggableId={teamId!}
index={teamIndex!}
>
{(provided, snapshot) => {
return (
<div
className='draggable-team-container'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div
className={classNames([`team-container ${teamClass}`, {isDragging: snapshot.isDragging}])}
>
{teamButton}
{orderIndicator}
</div>
</div>
);
}}
</Draggable>
) : (
<div className={`team-container ${teamClass}`}>
{teamButton}
{orderIndicator}
</div>
);
}
} }
export default injectIntl(TeamButton); function WithTeamTooltip({
order,
tip,
url,
children,
}: React.PropsWithChildren<Pick<Props, 'order' | 'tip' | 'url'>>) {
const intl = useIntl();
const shortcut = useMemo(() => {
if (!order || order >= 10) {
return undefined;
}
return {
default: [ShortcutKeys.ctrl, ShortcutKeys.alt, order.toString()],
mac: [ShortcutKeys.cmd, ShortcutKeys.option, order.toString()],
};
}, [order]);
return (
<WithTooltip
id={`tooltip-${url}`}
title={tip || intl.formatMessage(messages.nameUndefined)}
shortcut={shortcut}
placement='right'
>
{children}
</WithTooltip>
);
}

Просмотреть файл

@@ -3,55 +3,97 @@
import React from 'react'; import React from 'react';
import type {ComponentProps} from 'react'; import type {ComponentProps} from 'react';
import type {MessageDescriptor} from 'react-intl'; import {type MessageDescriptor} from 'react-intl';
import RenderEmoji from 'components/emoji/render_emoji'; import RenderEmoji from 'components/emoji/render_emoji';
import {ShortcutKey, ShortcutKeyVariant} from 'components/shortcut_key';
import Tooltip from 'components/tooltip'; import Tooltip from 'components/tooltip';
import {getStringOrDescriptorComponent} from './utils'; import {TooltipShortcutSequence, type ShortcutDefinition} from './shortcut';
import {getAsFormattedMessage} from './utils';
type EmojiStyle = 'inline' | 'large' | undefined;
export type CommonTooltipProps = { export type CommonTooltipProps = {
id: string; id: string;
title: string | MessageDescriptor; title: string | MessageDescriptor | React.ReactElement;
hint?: string | MessageDescriptor; hint?: string | MessageDescriptor | React.ReactElement;
shortcut?: string[]; shortcut?: ShortcutDefinition;
emoji?: string; emoji?: string;
emojiStyle?: EmojiStyle;
} }
export function createTooltip(commonTooltipProps: CommonTooltipProps) { export function createTooltip(commonTooltipProps: CommonTooltipProps) {
return (props: Omit<ComponentProps<typeof Tooltip>, 'children' | 'id'>) => { return (props: Omit<ComponentProps<typeof Tooltip>, 'children' | 'id'>) => {
const title = getStringOrDescriptorComponent(commonTooltipProps.title); const contents = [];
const hint = getStringOrDescriptorComponent(commonTooltipProps.hint);
if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle === 'large') {
contents.push(
<div
key='emoji'
className='tooltip-large-emoji'
>
<RenderEmoji
emojiName={commonTooltipProps.emoji}
size={48}
/>
</div>,
);
}
const title = getAsFormattedMessage(commonTooltipProps.title);
if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle !== 'large') {
contents.push(
<div
key='title'
className={'tooltip-title'}
>
<RenderEmoji
emojiName={commonTooltipProps.emoji}
size={16}
/>
{title}
</div>,
);
} else {
contents.push(
<div
key='title'
className={'tooltip-title'}
>
{title}
</div>,
);
}
if (commonTooltipProps.shortcut) {
contents.push(
<div
key='shortcut'
className={'tooltip-shortcuts-container'}
>
<TooltipShortcutSequence shortcut={commonTooltipProps.shortcut}/>
</div>,
);
}
const hint = getAsFormattedMessage(commonTooltipProps.hint);
if (commonTooltipProps.hint) {
contents.push(
<div
key='hint'
className={'tooltip-hint'}
>
{hint}
</div>,
);
}
const emoji = commonTooltipProps.emoji && (
<RenderEmoji
emojiName={commonTooltipProps.emoji}
size={12}
/>
);
return ( return (
<Tooltip <Tooltip
{...props} {...props}
id={commonTooltipProps.id} id={commonTooltipProps.id}
> >
<div className={'tooltip-title'}> {contents}
{emoji}
{title}
</div>
{commonTooltipProps.shortcut && (
<div className={'tooltip-shortcuts-container'}>
{commonTooltipProps.shortcut.map((v) => (
<ShortcutKey
key={v}
variant={ShortcutKeyVariant.Tooltip}
>
{v}
</ShortcutKey>
))}
</div>
)}
{commonTooltipProps.hint && (<div className={'tooltip-hint'}>{hint}</div>)}
</Tooltip> </Tooltip>
); );
}; };

Просмотреть файл

@@ -16,29 +16,34 @@ type OverlayTriggerProps = ComponentProps<typeof OverlayTrigger>;
type WithTooltipProps = { type WithTooltipProps = {
children: OverlayTriggerProps['children']; children: OverlayTriggerProps['children'];
placement: OverlayTriggerProps['placement']; placement: OverlayTriggerProps['placement'];
onShow?: () => void;
} & CommonTooltipProps; } & CommonTooltipProps;
const WithTooltip = ({ const WithTooltip = ({
id, id,
title, title,
emoji, emoji,
emojiStyle,
hint, hint,
shortcut, shortcut,
placement, placement,
onShow,
children, children,
}: WithTooltipProps) => { }: WithTooltipProps) => {
const ThisTooltip = useMemo(() => createTooltip({ const ThisTooltip = useMemo(() => createTooltip({
id, id,
title, title,
emoji, emoji,
emojiStyle,
hint, hint,
shortcut, shortcut,
}), [id, title, emoji, hint, shortcut]); }), [id, title, emoji, emojiStyle, hint, shortcut]);
return ( return (
<OverlayTrigger <OverlayTrigger
delay={Constants.OVERLAY_TIME_DELAY} delay={Constants.OVERLAY_TIME_DELAY}
overlay={<ThisTooltip/>} overlay={<ThisTooltip/>}
placement={placement} placement={placement}
onEnter={onShow}
> >
{children} {children}
</OverlayTrigger> </OverlayTrigger>

Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage, defineMessage} from 'react-intl';
import {ShortcutKey, ShortcutKeyVariant} from 'components/shortcut_key';
import {isMessageDescriptor} from 'utils/i18n';
import {isMac} from 'utils/user_agent';
export type ShortcutDefinition = {
default: ShortcutKeyDescriptor[];
mac?: ShortcutKeyDescriptor[];
}
export type ShortcutKeyDescriptor = string | MessageDescriptor;
export const ShortcutKeys = {
alt: defineMessage({
id: 'shortcuts.generic.alt',
defaultMessage: 'Alt',
}),
cmd: '⌘',
ctrl: defineMessage({
id: 'shortcuts.generic.ctrl',
defaultMessage: 'Ctrl',
}),
option: '⌥',
shift: defineMessage({
id: 'shortcuts.generic.shift',
defaultMessage: 'Shift',
}),
};
type Props = {
shortcut: ShortcutDefinition;
}
export function TooltipShortcutSequence(props: Props) {
let shortcut = props.shortcut.default;
if (props.shortcut.mac && isMac()) {
shortcut = props.shortcut.mac;
}
return (
<>
{shortcut.map((v) => {
let key;
let content;
if (isMessageDescriptor(v)) {
key = v.id;
content = <FormattedMessage {...v}/>;
} else {
key = v;
content = v;
}
return (
<ShortcutKey
key={key}
variant={ShortcutKeyVariant.Tooltip}
>
{content}
</ShortcutKey>
);
})}
</>
);
}

Просмотреть файл

@@ -6,19 +6,21 @@ import type {ComponentProps} from 'react';
import type {MessageDescriptor} from 'react-intl'; import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
export function getStringOrDescriptorComponent(v: string | MessageDescriptor | undefined, values?: ComponentProps<typeof FormattedMessage>['values']) { import {isMessageDescriptor} from 'utils/i18n';
export function getAsFormattedMessage(v: string | MessageDescriptor | React.ReactElement | undefined, values?: ComponentProps<typeof FormattedMessage>['values']) {
if (!v) { if (!v) {
return undefined; return undefined;
} }
if (typeof v === 'string') { if (isMessageDescriptor(v)) {
return v; return (
<FormattedMessage
{...v}
values={values}
/>
);
} }
return ( return v;
<FormattedMessage
{...v}
values={values}
/>
);
} }

Просмотреть файл

@@ -4953,6 +4953,9 @@
"shortcuts.files.header": "Files", "shortcuts.files.header": "Files",
"shortcuts.files.upload": "Upload files:\tCtrl|U", "shortcuts.files.upload": "Upload files:\tCtrl|U",
"shortcuts.files.upload.mac": "Upload files:\t⌘|U", "shortcuts.files.upload.mac": "Upload files:\t⌘|U",
"shortcuts.generic.alt": "Alt",
"shortcuts.generic.ctrl": "Ctrl",
"shortcuts.generic.shift": "Shift",
"shortcuts.header": "Keyboard shortcuts\tCtrl|/", "shortcuts.header": "Keyboard shortcuts\tCtrl|/",
"shortcuts.header.mac": "Keyboard shortcuts\t⌘|/", "shortcuts.header.mac": "Keyboard shortcuts\t⌘|/",
"shortcuts.info": "Begin a message with / for a list of all the available slash commands.", "shortcuts.info": "Begin a message with / for a list of all the available slash commands.",
@@ -5274,8 +5277,6 @@
"team.button.ariaLabel": "{teamName} team", "team.button.ariaLabel": "{teamName} team",
"team.button.mentions.ariaLabel": "{teamName} team, {mentionCount} mentions", "team.button.mentions.ariaLabel": "{teamName} team, {mentionCount} mentions",
"team.button.name_undefined": "This team does not have a name", "team.button.name_undefined": "This team does not have a name",
"team.button.tooltip": "Ctrl|Alt|{order}",
"team.button.tooltip.mac": "⌘|⌥|{order}",
"team.button.unread.ariaLabel": "{teamName} team unread", "team.button.unread.ariaLabel": "{teamName} team unread",
"terms_of_service.agreeButton": "I Agree", "terms_of_service.agreeButton": "I Agree",
"terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.", "terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.",

Просмотреть файл

@@ -29,6 +29,10 @@ export function getEmojiImageUrl(emoji: Emoji): string {
return Client4.getEmojiRoute(emoji.id) + '/image'; return Client4.getEmojiRoute(emoji.id) + '/image';
} }
export function getEmojiName(emoji: Emoji): string {
return isSystemEmoji(emoji) ? emoji.short_name : emoji.name;
}
export function parseEmojiNamesFromText(text: string): string[] { export function parseEmojiNamesFromText(text: string): string[] {
if (!text.includes(':')) { if (!text.includes(':')) {
return []; return [];

Просмотреть файл

@@ -50,9 +50,17 @@
color: rgba(255, 255, 255, 0.75); color: rgba(255, 255, 255, 0.75);
} }
.tooltip-large-emoji {
margin-bottom: 2px;
}
.tooltip-title { .tooltip-title {
font-weight: 600; font-weight: 600;
line-height: 15px; line-height: 15px;
.emoticon {
margin-right: 6px;
}
} }
.tooltip-hint { .tooltip-hint {
@@ -69,7 +77,6 @@
} }
.emoticon { .emoticon {
margin-right: 6px;
vertical-align: center; vertical-align: center;
} }
} }

Просмотреть файл

@@ -1,6 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type {MessageDescriptor} from 'react-intl';
export function isMessageDescriptor(descriptor: unknown): descriptor is MessageDescriptor {
return Boolean(descriptor && (descriptor as MessageDescriptor).id);
}
export function getMonthLong(locale: string): 'short' | 'long' { export function getMonthLong(locale: string): 'short' | 'long' {
if (locale === 'ko') { if (locale === 'ko') {
// Long and short are equivalent in Korean except long has a bug on IE11/Windows 7 // Long and short are equivalent in Korean except long has a bug on IE11/Windows 7