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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8c3ab07d49
Коммит
297135c5b4
@@ -7,6 +7,7 @@ import {Preferences as ReduxPreferences} from 'mattermost-redux/constants';
|
||||
import {getCustomEmojisByName as selectCustomEmojisByName, getCustomEmojisEnabled} from 'mattermost-redux/selectors/entities/emojis';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
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 {isCustomStatusEnabled, makeGetCustomStatus} from 'selectors/views/custom_status';
|
||||
@@ -68,16 +69,13 @@ export function addRecentEmojis(aliases) {
|
||||
|
||||
let updatedRecentEmojis = [...recentEmojis];
|
||||
for (const alias of aliases) {
|
||||
let name;
|
||||
const emoji = emojiMap.get(alias);
|
||||
if (!emoji) {
|
||||
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);
|
||||
if (currentEmojiIndexInRecentList > -1) {
|
||||
const currentEmojiInRecentList = updatedRecentEmojis[currentEmojiIndexInRecentList];
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#BillingSubscriptions__seatOverageTooltip {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.BillingSubscriptions__tooltip.tooltip {
|
||||
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 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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 {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 UpgradeSvg from 'components/common/svg_images_components/upgrade_svg';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
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 = (
|
||||
<div className='BillingSummary__noBillingHistory'>
|
||||
<EmptyBillingHistorySvg
|
||||
@@ -180,6 +190,7 @@ type InvoiceInfoProps = {
|
||||
|
||||
export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasMore, willRenew}: InvoiceInfoProps) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isUpcomingInvoice = invoice?.status.toLowerCase() === 'upcoming';
|
||||
const openInvoicePreview = () => {
|
||||
dispatch(
|
||||
@@ -298,32 +309,14 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges'
|
||||
defaultMessage='Partial charges'
|
||||
/>
|
||||
<OverlayTrigger
|
||||
delayShow={500}
|
||||
<WithTooltip
|
||||
id='BillingSubscriptions__seatOverageTooltip'
|
||||
title={messages.partialChargesTooltipTitle}
|
||||
hint={messages.partialChargesTooltipText}
|
||||
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'/>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
{partialCharges.map((charge: any) => (
|
||||
<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().handleEmojiClick({name: 'smile'} as any);
|
||||
wrapper.instance().handleEmojiClick(TestHelper.getCustomEmojiMock({name: 'smile'}));
|
||||
|
||||
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);
|
||||
expect(onUpdateCommentDraft).toHaveBeenCalled();
|
||||
@@ -252,7 +252,7 @@ describe('components/AdvancedCreateComment', () => {
|
||||
wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test', uploadsInProgress: [], fileInfos: []}),
|
||||
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
|
||||
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);
|
||||
@@ -264,7 +264,7 @@ describe('components/AdvancedCreateComment', () => {
|
||||
wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test ', uploadsInProgress: [], fileInfos: []}),
|
||||
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
|
||||
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
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 * as GlobalActions from 'actions/global_actions';
|
||||
@@ -471,7 +472,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
handleEmojiClick = (emoji: Emoji) => {
|
||||
const emojiAlias = ('short_name' in emoji && emoji.short_name) || emoji.name;
|
||||
const emojiAlias = getEmojiName(emoji);
|
||||
|
||||
if (!emojiAlias) {
|
||||
//Oops... There went something wrong
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
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 * as GlobalActions from 'actions/global_actions';
|
||||
@@ -1202,7 +1203,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
handleEmojiClick = (emoji: Emoji) => {
|
||||
const emojiAlias = ('short_names' in emoji && emoji.short_names && emoji.short_names[0]) || emoji.name;
|
||||
const emojiAlias = getEmojiName(emoji);
|
||||
|
||||
if (!emojiAlias) {
|
||||
//Oops.. There went something wrong
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import './multi_select_card.scss';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
export type Props = {
|
||||
onClick: () => void;
|
||||
@@ -49,21 +49,13 @@ const MultiSelectCard = (props: Props) => {
|
||||
|
||||
if (props.tooltip) {
|
||||
button = (
|
||||
<OverlayTrigger
|
||||
className='hidden-xs'
|
||||
delayShow={500}
|
||||
<WithTooltip
|
||||
id={props.id}
|
||||
placement='top'
|
||||
overlay={
|
||||
<Tooltip
|
||||
id={props.tooltip}
|
||||
className='hidden-xs'
|
||||
>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
title={props.tooltip}
|
||||
>
|
||||
{button}
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
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 {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
|
||||
|
||||
import DeletePostModal from 'components/delete_post_modal';
|
||||
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 emojiAlias = emoji && (((emoji as SystemEmoji).short_names && (emoji as SystemEmoji).short_names[0]) || emoji.name);
|
||||
if (!emoji) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojiAlias = getEmojiName(emoji);
|
||||
if (!emojiAlias) {
|
||||
//Oops.. There went something wrong
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,7 @@ import type InfiniteLoader from 'react-window-infinite-loader';
|
||||
|
||||
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 EmojiPickerCurrentResults from 'components/emoji_picker/components/emoji_picker_current_results';
|
||||
@@ -358,7 +358,7 @@ const EmojiPicker = ({
|
||||
return '';
|
||||
}
|
||||
|
||||
const name = isSystemEmoji(emoji) ? emoji.short_name : emoji.name;
|
||||
const name = getEmojiName(emoji);
|
||||
return name.replace(/_/g, ' ');
|
||||
}, [cursor.emojiId]);
|
||||
|
||||
|
||||
@@ -1,64 +1,5 @@
|
||||
// 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`] = `
|
||||
<Memo(KeyboardShortcutSequence)
|
||||
shortcut={
|
||||
|
||||
@@ -9,12 +9,6 @@ export type KeyboardShortcutDescriptor =
|
||||
| MessageDescriptor
|
||||
| {default: MessageDescriptor; mac?: MessageDescriptor};
|
||||
|
||||
export function isMessageDescriptor(
|
||||
descriptor: KeyboardShortcutDescriptor,
|
||||
): descriptor is MessageDescriptor {
|
||||
return Boolean((descriptor as MessageDescriptor).id);
|
||||
}
|
||||
|
||||
const callsKBShortcuts = {
|
||||
global: {
|
||||
callsJoinCall: {
|
||||
@@ -171,16 +165,6 @@ export const KEYBOARD_SHORTCUTS = {
|
||||
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: {
|
||||
default: {
|
||||
id: t('shortcuts.nav.switcher'),
|
||||
|
||||
@@ -7,8 +7,6 @@ import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import KeyboardShortcutsSequence from './keyboard_shortcuts_sequence';
|
||||
|
||||
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from './index';
|
||||
|
||||
describe('components/shortcuts/KeyboardShortcutsSequence', () => {
|
||||
test('should match snapshot when used for modal with description', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
@@ -95,21 +93,4 @@ describe('components/shortcuts/KeyboardShortcutsSequence', () => {
|
||||
expect(wrapper.find('.shortcut-key--tooltip')).toHaveLength(2);
|
||||
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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {FormatXMLElementFn, PrimitiveType} from 'intl-messageformat';
|
||||
import React, {memo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {ShortcutKeyVariant, ShortcutKey} from 'components/shortcut_key';
|
||||
|
||||
import {isMessageDescriptor} from 'utils/i18n';
|
||||
import {isMac} from 'utils/user_agent';
|
||||
|
||||
import {isMessageDescriptor} from './keyboard_shortcuts';
|
||||
import type {KeyboardShortcutDescriptor} from './keyboard_shortcuts';
|
||||
|
||||
import './keyboard_shortcuts_sequence.scss';
|
||||
|
||||
type Props = {
|
||||
shortcut: KeyboardShortcutDescriptor;
|
||||
values?: Record<string, PrimitiveType | FormatXMLElementFn<string, string>>;
|
||||
hideDescription?: boolean;
|
||||
hoistDescription?: boolean;
|
||||
isInsideTooltip?: boolean;
|
||||
@@ -32,9 +30,9 @@ function normalizeShortcutDescriptor(shortcut: KeyboardShortcutDescriptor) {
|
||||
|
||||
const KEY_SEPARATOR = '|';
|
||||
|
||||
function KeyboardShortcutSequence({shortcut, values, hideDescription, hoistDescription, isInsideTooltip}: Props) {
|
||||
function KeyboardShortcutSequence({shortcut, hideDescription, hoistDescription, isInsideTooltip}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const shortcutText = formatMessage(normalizeShortcutDescriptor(shortcut), values);
|
||||
const shortcutText = formatMessage(normalizeShortcutDescriptor(shortcut));
|
||||
const splitShortcut = shortcutText.split('\t');
|
||||
|
||||
let description = '';
|
||||
|
||||
@@ -17,27 +17,14 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
|
||||
target={[MockFunction]}
|
||||
topOffset={-7}
|
||||
/>
|
||||
<OverlayTrigger
|
||||
className="hidden-xs"
|
||||
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>
|
||||
}
|
||||
<WithTooltip
|
||||
id="reaction-icon-tooltip"
|
||||
placement="top"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
title={
|
||||
Object {
|
||||
"defaultMessage": "Add Reaction",
|
||||
"id": "post_info.tooltip.add_reactions",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
@@ -51,6 +38,6 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
|
||||
className="icon icon--small"
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</Connect(Component)>
|
||||
`;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import {shallow} from 'enzyme';
|
||||
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', () => {
|
||||
const baseProps = {
|
||||
@@ -31,7 +31,7 @@ describe('components/post_view/PostReaction', () => {
|
||||
const wrapper = shallow(<PostReaction {...baseProps}/>);
|
||||
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).toHaveBeenCalledWith('post_id_1', 'smile');
|
||||
expect(baseProps.toggleEmojiPicker).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -3,23 +3,30 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {defineMessages} from 'react-intl';
|
||||
|
||||
import type {Emoji} from '@mattermost/types/emojis';
|
||||
|
||||
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 OverlayTrigger from 'components/overlay_trigger';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {Locations} from 'utils/constants';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
const TOP_OFFSET = -7;
|
||||
|
||||
const messages = defineMessages({
|
||||
addReaction: {
|
||||
id: 'post_info.tooltip.add_reactions',
|
||||
defaultMessage: 'Add Reaction',
|
||||
},
|
||||
});
|
||||
|
||||
export type Props = {
|
||||
channelId?: string;
|
||||
postId: string;
|
||||
@@ -46,7 +53,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
|
||||
|
||||
handleToggleEmoji = (emoji: Emoji): void => {
|
||||
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.toggleEmojiPicker();
|
||||
};
|
||||
@@ -83,21 +90,10 @@ export default class PostReaction extends React.PureComponent<Props, State> {
|
||||
spaceRequiredAbove={spaceRequiredAbove}
|
||||
spaceRequiredBelow={spaceRequiredBelow}
|
||||
/>
|
||||
<OverlayTrigger
|
||||
className='hidden-xs'
|
||||
delayShow={500}
|
||||
<WithTooltip
|
||||
id='reaction-icon-tooltip'
|
||||
title={messages.addReaction}
|
||||
placement='top'
|
||||
overlay={
|
||||
<Tooltip
|
||||
id='reaction-icon-tooltip'
|
||||
className='hidden-xs'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.tooltip.add_reactions'
|
||||
defaultMessage='Add Reaction'
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<button
|
||||
data-testid='post-reaction-emoji-icon'
|
||||
@@ -110,7 +106,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
|
||||
>
|
||||
<EmojiIcon className='icon icon--small'/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</React.Fragment>
|
||||
</ChannelPermissionGate>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,10 @@ import React from 'react';
|
||||
import type {Emoji} from '@mattermost/types/emojis';
|
||||
|
||||
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 Tooltip from 'components/tooltip';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {Locations} from 'utils/constants';
|
||||
|
||||
@@ -43,7 +42,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -70,7 +69,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
|
||||
function capitalizeFirstLetter(s: string) {
|
||||
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, ' '));
|
||||
};
|
||||
|
||||
@@ -92,25 +91,12 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.ADD_REACTION]}
|
||||
>
|
||||
<OverlayTrigger
|
||||
className='hidden-xs'
|
||||
delayShow={500}
|
||||
<WithTooltip
|
||||
id='post_info.emoji.tooltip'
|
||||
title={this.emojiName(emoji, this.props.locale)}
|
||||
emoji={getEmojiName(emoji)}
|
||||
emojiStyle='large'
|
||||
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>
|
||||
<React.Fragment>
|
||||
@@ -121,7 +107,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
|
||||
/>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</ChannelPermissionGate>
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {useIntl} from 'react-intl';
|
||||
|
||||
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 = {
|
||||
emoji: Emoji;
|
||||
@@ -24,7 +24,7 @@ const EmojiItem = ({emoji, onItemClick, order}: Props) => {
|
||||
|
||||
const itemClassName = 'post-menu__item';
|
||||
|
||||
const emojiName = isSystemEmoji(emoji) ? emoji.short_name ?? emoji.name : emoji.name;
|
||||
const emojiName = getEmojiName(emoji);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,50 +1,27 @@
|
||||
// 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`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={500}
|
||||
onEnter={[Function]}
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="post_id-smile-reaction"
|
||||
>
|
||||
<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={
|
||||
<Connect(ReactionTooltip)
|
||||
canAddReactions={false}
|
||||
canRemoveReactions={true}
|
||||
currentUserReacted={false}
|
||||
emojiName="smile"
|
||||
id="post_id-smile-reaction"
|
||||
onShow={[Function]}
|
||||
reactions={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
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",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -87,54 +64,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</Connect(ReactionTooltip)>
|
||||
`;
|
||||
|
||||
exports[`components/post_view/Reaction should apply read-only class if user does not have permission to remove reaction 1`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={500}
|
||||
onEnter={[Function]}
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="post_id-smile-reaction"
|
||||
>
|
||||
<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={
|
||||
<Connect(ReactionTooltip)
|
||||
canAddReactions={true}
|
||||
canRemoveReactions={false}
|
||||
currentUserReacted={true}
|
||||
emojiName="smile"
|
||||
id="post_id-smile-reaction"
|
||||
onShow={[Function]}
|
||||
reactions={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
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",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -177,54 +131,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</Connect(ReactionTooltip)>
|
||||
`;
|
||||
|
||||
exports[`components/post_view/Reaction should match snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={500}
|
||||
onEnter={[Function]}
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="post_id-smile-reaction"
|
||||
>
|
||||
<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={
|
||||
<Connect(ReactionTooltip)
|
||||
canAddReactions={true}
|
||||
canRemoveReactions={true}
|
||||
currentUserReacted={false}
|
||||
emojiName="smile"
|
||||
id="post_id-smile-reaction"
|
||||
onShow={[Function]}
|
||||
reactions={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
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",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -267,54 +198,31 @@ exports[`components/post_view/Reaction should match snapshot 1`] = `
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</Connect(ReactionTooltip)>
|
||||
`;
|
||||
|
||||
exports[`components/post_view/Reaction should match snapshot when a current user reacted to a post 1`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={500}
|
||||
onEnter={[Function]}
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="post_id-smile-reaction"
|
||||
>
|
||||
<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={
|
||||
<Connect(ReactionTooltip)
|
||||
canAddReactions={true}
|
||||
canRemoveReactions={true}
|
||||
currentUserReacted={true}
|
||||
emojiName="smile"
|
||||
id="post_id-smile-reaction"
|
||||
onShow={[Function]}
|
||||
reactions={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
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",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -357,7 +265,7 @@ exports[`components/post_view/Reaction should match snapshot when a current user
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</Connect(ReactionTooltip)>
|
||||
`;
|
||||
|
||||
exports[`components/post_view/Reaction should return null/empty if no emojiImageUrl 1`] = `""`;
|
||||
|
||||
@@ -71,14 +71,6 @@
|
||||
margin: 0 2px 0 0;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
|
||||
&--large {
|
||||
width: 48px;
|
||||
max-width: none;
|
||||
height: 48px;
|
||||
max-height: none;
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__emoji--post-menu {
|
||||
|
||||
@@ -6,9 +6,6 @@ import React from 'react';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
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 ReactionTooltip from './reaction_tooltip';
|
||||
@@ -218,23 +215,14 @@ export default class Reaction extends React.PureComponent<Props, State> {
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
delayShow={500}
|
||||
placement='top'
|
||||
shouldUpdatePosition={true}
|
||||
overlay={
|
||||
<Tooltip id={`${this.props.post.id}-${this.props.emojiName}-reaction`}>
|
||||
<ReactionTooltip
|
||||
canAddReactions={canAddReactions}
|
||||
canRemoveReactions={canRemoveReactions}
|
||||
currentUserReacted={currentUserReacted}
|
||||
emojiName={emojiName}
|
||||
emojiIcon={emojiIcon}
|
||||
reactions={reactions}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
onEnter={this.loadMissingProfiles}
|
||||
<ReactionTooltip
|
||||
id={`${this.props.post.id}-${this.props.emojiName}-reaction`}
|
||||
canAddReactions={canAddReactions}
|
||||
canRemoveReactions={canRemoveReactions}
|
||||
currentUserReacted={currentUserReacted}
|
||||
emojiName={emojiName}
|
||||
reactions={reactions}
|
||||
onShow={this.loadMissingProfiles}
|
||||
>
|
||||
<button
|
||||
id={`postReaction-${this.props.post.id}-${this.props.emojiName}`}
|
||||
@@ -262,7 +250,7 @@ export default class Reaction extends React.PureComponent<Props, State> {
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</ReactionTooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
|
||||
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
type Props = {
|
||||
canAddReactions: boolean;
|
||||
canRemoveReactions: boolean;
|
||||
children: React.ReactNode;
|
||||
currentUserReacted: boolean;
|
||||
emojiName: string;
|
||||
emojiIcon: React.ReactNode;
|
||||
id: string;
|
||||
onShow: () => void;
|
||||
reactions: ReactionType[];
|
||||
users: string[];
|
||||
};
|
||||
@@ -20,123 +24,120 @@ const ReactionTooltip: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
canAddReactions,
|
||||
canRemoveReactions,
|
||||
children,
|
||||
currentUserReacted,
|
||||
emojiIcon,
|
||||
emojiName,
|
||||
id,
|
||||
onShow,
|
||||
reactions,
|
||||
users,
|
||||
} = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const otherUsersCount = reactions.length - users.length;
|
||||
|
||||
let names: React.ReactNode;
|
||||
let names;
|
||||
if (otherUsersCount > 0) {
|
||||
if (users.length > 0) {
|
||||
names = (
|
||||
<FormattedMessage
|
||||
id='reaction.usersAndOthersReacted'
|
||||
defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}'
|
||||
values={{
|
||||
users: users.join(', '),
|
||||
otherUsers: otherUsersCount,
|
||||
}}
|
||||
/>
|
||||
names = intl.formatMessage(
|
||||
{
|
||||
id: 'reaction.usersAndOthersReacted',
|
||||
defaultMessage: '{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}',
|
||||
},
|
||||
{
|
||||
users: users.join(', '),
|
||||
otherUsers: otherUsersCount,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
names = (
|
||||
<FormattedMessage
|
||||
id='reaction.othersReacted'
|
||||
defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}'
|
||||
values={{
|
||||
otherUsers: otherUsersCount,
|
||||
}}
|
||||
/>
|
||||
names = intl.formatMessage(
|
||||
{
|
||||
id: 'reaction.othersReacted',
|
||||
defaultMessage: '{otherUsers, number} {otherUsers, plural, one {user} other {users}}',
|
||||
},
|
||||
{
|
||||
otherUsers: otherUsersCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (users.length > 1) {
|
||||
names = (
|
||||
<FormattedMessage
|
||||
id='reaction.usersReacted'
|
||||
defaultMessage='{users} and {lastUser}'
|
||||
values={{
|
||||
users: users.slice(0, -1).join(', '),
|
||||
lastUser: users[users.length - 1],
|
||||
}}
|
||||
/>
|
||||
names = intl.formatMessage(
|
||||
{
|
||||
id: 'reaction.usersReacted',
|
||||
defaultMessage: '{users} and {lastUser}',
|
||||
},
|
||||
{
|
||||
users: users.slice(0, -1).join(', '),
|
||||
lastUser: users[users.length - 1],
|
||||
},
|
||||
);
|
||||
} else {
|
||||
names = users[0];
|
||||
}
|
||||
|
||||
let reactionVerb: React.ReactNode;
|
||||
let reactionVerb;
|
||||
if (users.length + otherUsersCount > 1) {
|
||||
if (currentUserReacted) {
|
||||
reactionVerb = (
|
||||
<FormattedMessage
|
||||
id='reaction.reactionVerb.youAndUsers'
|
||||
defaultMessage='reacted'
|
||||
/>
|
||||
);
|
||||
reactionVerb = intl.formatMessage({
|
||||
id: 'reaction.reactionVerb.youAndUsers',
|
||||
defaultMessage: 'reacted',
|
||||
});
|
||||
} else {
|
||||
reactionVerb = (
|
||||
<FormattedMessage
|
||||
id='reaction.reactionVerb.users'
|
||||
defaultMessage='reacted'
|
||||
/>
|
||||
);
|
||||
reactionVerb = intl.formatMessage({
|
||||
id: 'reaction.reactionVerb.users',
|
||||
defaultMessage: 'reacted',
|
||||
});
|
||||
}
|
||||
} else if (currentUserReacted) {
|
||||
reactionVerb = (
|
||||
<FormattedMessage
|
||||
id='reaction.reactionVerb.you'
|
||||
defaultMessage='reacted'
|
||||
/>
|
||||
);
|
||||
reactionVerb = intl.formatMessage({
|
||||
id: 'reaction.reactionVerb.you',
|
||||
defaultMessage: 'reacted',
|
||||
});
|
||||
} else {
|
||||
reactionVerb = (
|
||||
<FormattedMessage
|
||||
id='reaction.reactionVerb.user'
|
||||
defaultMessage='reacted'
|
||||
/>
|
||||
);
|
||||
reactionVerb = intl.formatMessage({
|
||||
id: 'reaction.reactionVerb.user',
|
||||
defaultMessage: 'reacted',
|
||||
});
|
||||
}
|
||||
|
||||
const tooltip = (
|
||||
<FormattedMessage
|
||||
id='reaction.reacted'
|
||||
defaultMessage='{users} {reactionVerb} with {emoji}'
|
||||
values={{
|
||||
users: <b>{names}</b>,
|
||||
reactionVerb,
|
||||
emoji: <b>{':' + emojiName + ':'}</b>,
|
||||
}}
|
||||
/>
|
||||
const tooltip = intl.formatMessage(
|
||||
{
|
||||
id: 'reaction.reacted',
|
||||
defaultMessage: '{users} {reactionVerb} with {emoji}',
|
||||
},
|
||||
{
|
||||
users: names,
|
||||
reactionVerb,
|
||||
emoji: ':' + emojiName + ':',
|
||||
},
|
||||
);
|
||||
|
||||
let clickTooltip: React.ReactNode;
|
||||
let clickTooltip;
|
||||
if (currentUserReacted && canRemoveReactions) {
|
||||
clickTooltip = (
|
||||
<FormattedMessage
|
||||
id='reaction.clickToRemove'
|
||||
defaultMessage='(click to remove)'
|
||||
/>
|
||||
);
|
||||
clickTooltip = intl.formatMessage({
|
||||
id: 'reaction.clickToRemove',
|
||||
defaultMessage: '(click to remove)',
|
||||
});
|
||||
} else if (!currentUserReacted && canAddReactions) {
|
||||
clickTooltip = (
|
||||
<FormattedMessage
|
||||
id='reaction.clickToAdd'
|
||||
defaultMessage='(click to add)'
|
||||
/>
|
||||
);
|
||||
clickTooltip = intl.formatMessage({
|
||||
id: 'reaction.clickToAdd',
|
||||
defaultMessage: '(click to add)',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='reaction-emoji--large'>{emojiIcon}</div>
|
||||
{tooltip}
|
||||
<br/>
|
||||
{clickTooltip}
|
||||
</>
|
||||
<WithTooltip
|
||||
id={id}
|
||||
emoji={emojiName}
|
||||
emojiStyle='large'
|
||||
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"
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={400}
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="addReactionTooltip"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Add a reaction"
|
||||
id="reaction_list.addReactionTooltip"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
<WithTooltip
|
||||
id="addReactionTooltip"
|
||||
placement="top"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
title={
|
||||
Object {
|
||||
"defaultMessage": "Add a reaction",
|
||||
"id": "reaction_list.addReactionTooltip",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
@@ -104,7 +93,7 @@ exports[`components/ReactionList should render when there are reactions 1`] = `
|
||||
<AddReactionIcon />
|
||||
</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</Connect(Component)>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,28 +2,33 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {defineMessages} from 'react-intl';
|
||||
|
||||
import type {Emoji} from '@mattermost/types/emojis';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
|
||||
|
||||
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 OverlayTrigger from 'components/overlay_trigger';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import Reaction from 'components/post_view/reaction';
|
||||
import Tooltip from 'components/tooltip';
|
||||
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';
|
||||
|
||||
const DEFAULT_EMOJI_PICKER_RIGHT_OFFSET = 15;
|
||||
const EMOJI_PICKER_WIDTH_OFFSET = 260;
|
||||
|
||||
const messages = defineMessages({
|
||||
addAReaction: {
|
||||
id: 'reaction_list.addReactionTooltip',
|
||||
defaultMessage: 'Add a reaction',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
@@ -90,7 +95,7 @@ export default class ReactionList extends React.PureComponent<Props, State> {
|
||||
|
||||
handleEmojiClick = (emoji: Emoji): void => {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -148,15 +153,6 @@ export default class ReactionList extends React.PureComponent<Props, State> {
|
||||
|
||||
let emojiPicker = null;
|
||||
if (this.props.canAddReactions) {
|
||||
const addReactionTooltip = (
|
||||
<Tooltip id='addReactionTooltip'>
|
||||
<FormattedMessage
|
||||
id='reaction_list.addReactionTooltip'
|
||||
defaultMessage='Add a reaction'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
emojiPicker = (
|
||||
<span className='emoji-picker__container'>
|
||||
<EmojiPickerOverlay
|
||||
@@ -172,10 +168,10 @@ export default class ReactionList extends React.PureComponent<Props, State> {
|
||||
teamId={this.props.teamId}
|
||||
permissions={[Permissions.ADD_REACTION]}
|
||||
>
|
||||
<OverlayTrigger
|
||||
<WithTooltip
|
||||
id='addReactionTooltip'
|
||||
title={messages.addAReaction}
|
||||
placement='top'
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
overlay={addReactionTooltip}
|
||||
>
|
||||
<button
|
||||
aria-label={localizeMessage('reaction.add.ariaLabel', 'Add a reaction')}
|
||||
@@ -190,7 +186,7 @@ export default class ReactionList extends React.PureComponent<Props, State> {
|
||||
<AddReactionIcon/>
|
||||
</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</ChannelPermissionGate>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -545,16 +545,3 @@
|
||||
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.
|
||||
|
||||
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 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 CloudInvoicePreview from 'components/cloud_invoice_preview';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import type {Seats} 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 WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {BillingSchemes, ModalIdentifiers, TELEMETRY_CATEGORIES, CloudLinks} from 'utils/constants';
|
||||
|
||||
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 = {
|
||||
invoice: Invoice;
|
||||
product?: Product;
|
||||
@@ -39,6 +49,7 @@ type RenewalCardProps = {
|
||||
|
||||
export default function RenewalCard({invoice, product, hasMore, fullCharges, partialCharges, seats, onSeatChange, existingUsers, buttonDisabled, onButtonClick}: RenewalCardProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openInvoicePreview = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
@@ -178,32 +189,14 @@ export default function RenewalCard({invoice, product, hasMore, fullCharges, par
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges'
|
||||
defaultMessage='Partial charges'
|
||||
/>
|
||||
<OverlayTrigger
|
||||
delayShow={500}
|
||||
<WithTooltip
|
||||
id='BillingSubscriptions__seatOverageTooltip'
|
||||
title={messages.partialChargesTooltipTitle}
|
||||
hint={messages.partialChargesTooltipText}
|
||||
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'/>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
{partialCharges.map((charge: any) => (
|
||||
<div
|
||||
|
||||
@@ -2,18 +2,28 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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 OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
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';
|
||||
|
||||
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 {
|
||||
price: number;
|
||||
seats: Seats;
|
||||
@@ -154,25 +164,6 @@ export default function SeatsCalculator(props: Props) {
|
||||
|
||||
const maxSeats = calculateMaxUsers(annualPricePerSeat);
|
||||
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 (
|
||||
<div className='SeatsCalculator'>
|
||||
@@ -198,16 +189,17 @@ export default function SeatsCalculator(props: Props) {
|
||||
</div>
|
||||
<div className='SeatsCalculator__seats-tooltip'>
|
||||
<div className='icon'>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
<WithTooltip
|
||||
id='userCount__tooltip'
|
||||
title={messages.tooltipTitle}
|
||||
hint={messages.tooltipText}
|
||||
placement='right'
|
||||
overlay={userCountTooltip}
|
||||
>
|
||||
<InformationOutlineIcon
|
||||
size={18}
|
||||
color={'rgba(var(--center-channel-text-rgb), 0.75)'}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,6 +234,5 @@ export default function SeatsCalculator(props: Props) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,39 +4,37 @@ exports[`components/sidebar/channel_filter should match snapshot 1`] = `
|
||||
<div
|
||||
className="SidebarFilters"
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
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>
|
||||
}
|
||||
<WithTooltip
|
||||
id="channel-filter-tooltip"
|
||||
placement="right"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
shortcut={
|
||||
Object {
|
||||
"default": Array [
|
||||
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
|
||||
@@ -49,7 +47,7 @@ exports[`components/sidebar/channel_filter should match snapshot 1`] = `
|
||||
className="icon icon-filter-variant"
|
||||
/>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -57,39 +55,37 @@ exports[`components/sidebar/channel_filter should match snapshot if the unread f
|
||||
<div
|
||||
className="SidebarFilters"
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
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>
|
||||
}
|
||||
<WithTooltip
|
||||
id="channel-filter-tooltip"
|
||||
placement="right"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
shortcut={
|
||||
Object {
|
||||
"default": Array [
|
||||
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
|
||||
@@ -102,6 +98,6 @@ exports[`components/sidebar/channel_filter should match snapshot if the unread f
|
||||
className="icon icon-filter-variant"
|
||||
/>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3,18 +3,33 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {injectIntl} from 'react-intl';
|
||||
import {defineMessages, injectIntl} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
import {ShortcutKeys} from 'components/with_tooltip/shortcut';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
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 = {
|
||||
intl: IntlShape;
|
||||
hasMultipleTeams: boolean;
|
||||
@@ -62,34 +77,15 @@ export class ChannelFilter extends React.PureComponent<Props> {
|
||||
render() {
|
||||
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 tooltip = (
|
||||
<Tooltip
|
||||
id='new-group-tooltip'
|
||||
className='hidden-xs'
|
||||
>
|
||||
{tooltipMessage}
|
||||
<KeyboardShortcutSequence
|
||||
shortcut={KEYBOARD_SHORTCUTS.navToggleUnreads}
|
||||
hideDescription={true}
|
||||
isInsideTooltip={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='SidebarFilters'>
|
||||
<OverlayTrigger
|
||||
delayShow={500}
|
||||
<WithTooltip
|
||||
id='channel-filter-tooltip'
|
||||
title={unreadFilterEnabled ? messages.disableTooltip : messages.enableTooltip}
|
||||
shortcut={shortcut}
|
||||
placement={hasMultipleTeams ? 'top' : 'right'}
|
||||
overlay={tooltip}
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
@@ -101,7 +97,7 @@ export class ChannelFilter extends React.PureComponent<Props> {
|
||||
>
|
||||
<i className='icon icon-filter-variant'/>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,25 +2,26 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {Draggable} from 'react-beautiful-dnd';
|
||||
import {injectIntl} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import {mark, trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
|
||||
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 WithTooltip from 'components/with_tooltip';
|
||||
import {ShortcutKeys} from 'components/with_tooltip/shortcut';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
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 {
|
||||
btnClass?: string;
|
||||
@@ -36,7 +37,6 @@ interface Props {
|
||||
placement?: 'left' | 'right' | 'top' | 'bottom';
|
||||
teamIconUrl?: string | null;
|
||||
switchTeam: (url: string) => void;
|
||||
intl: IntlShape;
|
||||
isDraggable?: boolean;
|
||||
teamIndex?: number;
|
||||
teamId?: string;
|
||||
@@ -44,178 +44,201 @@ interface Props {
|
||||
hasUrgent?: boolean;
|
||||
}
|
||||
|
||||
class TeamButton extends React.PureComponent<Props> {
|
||||
handleSwitch = (e: React.MouseEvent) => {
|
||||
export default function TeamButton({
|
||||
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');
|
||||
e.preventDefault();
|
||||
this.props.switchTeam(this.props.url);
|
||||
switchTeam(url);
|
||||
|
||||
setTimeout(() => {
|
||||
trackEvent('ui', 'ui_team_sidebar_switch_team');
|
||||
}, 0);
|
||||
};
|
||||
}, [switchTeam, url]);
|
||||
|
||||
render() {
|
||||
const {teamIconUrl, displayName, btnClass, mentions, unread, isDraggable = false, teamIndex, teamId, order} = this.props;
|
||||
const {formatMessage} = this.props.intl;
|
||||
let teamClass: string = otherProps.active ? 'active' : '';
|
||||
const isNotCreateTeamButton: boolean = !url.endsWith('create_team') && !url.endsWith('select_team');
|
||||
|
||||
let teamClass: string = this.props.active ? 'active' : '';
|
||||
const isNotCreateTeamButton: boolean = !this.props.url.endsWith('create_team') && !this.props.url.endsWith('select_team');
|
||||
let badge: JSX.Element | undefined;
|
||||
|
||||
let badge: JSX.Element | undefined;
|
||||
let ariaLabel = formatMessage({
|
||||
id: 'team.button.ariaLabel',
|
||||
defaultMessage: '{teamName} team',
|
||||
},
|
||||
{
|
||||
teamName: displayName,
|
||||
});
|
||||
|
||||
let ariaLabel = formatMessage({
|
||||
id: 'team.button.ariaLabel',
|
||||
defaultMessage: '{teamName} team',
|
||||
if (!teamClass) {
|
||||
if (unread && !otherProps.isInProduct) {
|
||||
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,
|
||||
});
|
||||
|
||||
if (!teamClass) {
|
||||
if (unread && !this.props.isInProduct) {
|
||||
teamClass = 'unread';
|
||||
|
||||
badge = (
|
||||
<span className={'unread-badge'}/>
|
||||
);
|
||||
} else if (isNotCreateTeamButton) {
|
||||
teamClass = '';
|
||||
} else {
|
||||
teamClass = 'special';
|
||||
}
|
||||
if (mentions) {
|
||||
ariaLabel = formatMessage({
|
||||
id: 'team.button.unread.ariaLabel',
|
||||
defaultMessage: '{teamName} team unread',
|
||||
id: 'team.button.mentions.ariaLabel',
|
||||
defaultMessage: '{teamName} team, {mentionCount} mentions',
|
||||
},
|
||||
{
|
||||
teamName: displayName,
|
||||
mentionCount: mentions,
|
||||
});
|
||||
|
||||
if (mentions) {
|
||||
ariaLabel = formatMessage({
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
badge = (
|
||||
<span className={classNames('badge badge-max-number pull-right small', {urgent: otherProps.hasUrgent})}>{mentions > 99 ? '99+' : mentions}</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.showOrder) {
|
||||
orderIndicator = (
|
||||
<div className='order-indicator'>
|
||||
{order}
|
||||
ariaLabel = ariaLabel.toLowerCase();
|
||||
|
||||
const content = (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const btn = (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement={this.props.placement}
|
||||
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>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</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 type {ComponentProps} from 'react';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import {type MessageDescriptor} from 'react-intl';
|
||||
|
||||
import RenderEmoji from 'components/emoji/render_emoji';
|
||||
import {ShortcutKey, ShortcutKeyVariant} from 'components/shortcut_key';
|
||||
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 = {
|
||||
id: string;
|
||||
title: string | MessageDescriptor;
|
||||
hint?: string | MessageDescriptor;
|
||||
shortcut?: string[];
|
||||
title: string | MessageDescriptor | React.ReactElement;
|
||||
hint?: string | MessageDescriptor | React.ReactElement;
|
||||
shortcut?: ShortcutDefinition;
|
||||
emoji?: string;
|
||||
emojiStyle?: EmojiStyle;
|
||||
}
|
||||
|
||||
export function createTooltip(commonTooltipProps: CommonTooltipProps) {
|
||||
return (props: Omit<ComponentProps<typeof Tooltip>, 'children' | 'id'>) => {
|
||||
const title = getStringOrDescriptorComponent(commonTooltipProps.title);
|
||||
const hint = getStringOrDescriptorComponent(commonTooltipProps.hint);
|
||||
const contents = [];
|
||||
|
||||
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 (
|
||||
<Tooltip
|
||||
{...props}
|
||||
id={commonTooltipProps.id}
|
||||
>
|
||||
<div className={'tooltip-title'}>
|
||||
{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>)}
|
||||
{contents}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,29 +16,34 @@ type OverlayTriggerProps = ComponentProps<typeof OverlayTrigger>;
|
||||
type WithTooltipProps = {
|
||||
children: OverlayTriggerProps['children'];
|
||||
placement: OverlayTriggerProps['placement'];
|
||||
onShow?: () => void;
|
||||
} & CommonTooltipProps;
|
||||
const WithTooltip = ({
|
||||
id,
|
||||
title,
|
||||
emoji,
|
||||
emojiStyle,
|
||||
hint,
|
||||
shortcut,
|
||||
placement,
|
||||
onShow,
|
||||
children,
|
||||
}: WithTooltipProps) => {
|
||||
const ThisTooltip = useMemo(() => createTooltip({
|
||||
id,
|
||||
title,
|
||||
emoji,
|
||||
emojiStyle,
|
||||
hint,
|
||||
shortcut,
|
||||
}), [id, title, emoji, hint, shortcut]);
|
||||
}), [id, title, emoji, emojiStyle, hint, shortcut]);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
delay={Constants.OVERLAY_TIME_DELAY}
|
||||
overlay={<ThisTooltip/>}
|
||||
placement={placement}
|
||||
onEnter={onShow}
|
||||
>
|
||||
{children}
|
||||
</OverlayTrigger>
|
||||
|
||||
70
webapp/channels/src/components/with_tooltip/shortcut.tsx
Обычный файл
70
webapp/channels/src/components/with_tooltip/shortcut.tsx
Обычный файл
@@ -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 {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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof v === 'string') {
|
||||
return v;
|
||||
if (isMessageDescriptor(v)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
{...v}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
{...v}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -4953,6 +4953,9 @@
|
||||
"shortcuts.files.header": "Files",
|
||||
"shortcuts.files.upload": "Upload files:\tCtrl|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.mac": "Keyboard shortcuts\t⌘|/",
|
||||
"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.mentions.ariaLabel": "{teamName} team, {mentionCount} mentions",
|
||||
"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",
|
||||
"terms_of_service.agreeButton": "I Agree",
|
||||
"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';
|
||||
}
|
||||
|
||||
export function getEmojiName(emoji: Emoji): string {
|
||||
return isSystemEmoji(emoji) ? emoji.short_name : emoji.name;
|
||||
}
|
||||
|
||||
export function parseEmojiNamesFromText(text: string): string[] {
|
||||
if (!text.includes(':')) {
|
||||
return [];
|
||||
|
||||
@@ -50,9 +50,17 @@
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.tooltip-large-emoji {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-weight: 600;
|
||||
line-height: 15px;
|
||||
|
||||
.emoticon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-hint {
|
||||
@@ -69,7 +77,6 @@
|
||||
}
|
||||
|
||||
.emoticon {
|
||||
margin-right: 6px;
|
||||
vertical-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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' {
|
||||
if (locale === 'ko') {
|
||||
// Long and short are equivalent in Korean except long has a bug on IE11/Windows 7
|
||||
|
||||
Ссылка в новой задаче
Block a user