diff --git a/webapp/channels/src/actions/emoji_actions.js b/webapp/channels/src/actions/emoji_actions.js index 553cd74281..1cd4d420af 100644 --- a/webapp/channels/src/actions/emoji_actions.js +++ b/webapp/channels/src/actions/emoji_actions.js @@ -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]; diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/billing_subscriptions.scss b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/billing_subscriptions.scss index a1bec3b689..a7e20e7412 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/billing_subscriptions.scss +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/billing_subscriptions.scss @@ -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; } diff --git a/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx b/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx index 8c9cde5748..ae53b2359e 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx @@ -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 = (
{ 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' /> - -
- -
-
- -
- - } > -
+
{partialCharges.map((charge: any) => (
{ (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); diff --git a/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx b/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx index 4c5b529176..67ec9a3245 100644 --- a/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx +++ b/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx @@ -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 { }; 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 diff --git a/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx b/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx index 8ac9271fb6..b9291e1d1e 100644 --- a/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx +++ b/webapp/channels/src/components/advanced_create_post/advanced_create_post.tsx @@ -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 { }; 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 diff --git a/webapp/channels/src/components/common/multi_select_cards/multi_select_card.tsx b/webapp/channels/src/components/common/multi_select_cards/multi_select_card.tsx index 540ba660df..6eafac6561 100644 --- a/webapp/channels/src/components/common/multi_select_cards/multi_select_card.tsx +++ b/webapp/channels/src/components/common/multi_select_cards/multi_select_card.tsx @@ -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 = ( - - {props.tooltip} - - } + title={props.tooltip} > {button} - + ); } diff --git a/webapp/channels/src/components/edit_post/edit_post.tsx b/webapp/channels/src/components/edit_post/edit_post.tsx index 531cedd8c5..02120586bd 100644 --- a/webapp/channels/src/components/edit_post/edit_post.tsx +++ b/webapp/channels/src/components/edit_post/edit_post.tsx @@ -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; diff --git a/webapp/channels/src/components/emoji_picker/emoji_picker.tsx b/webapp/channels/src/components/emoji_picker/emoji_picker.tsx index db1705aa26..78338a3890 100644 --- a/webapp/channels/src/components/emoji_picker/emoji_picker.tsx +++ b/webapp/channels/src/components/emoji_picker/emoji_picker.tsx @@ -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]); diff --git a/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/__snapshots__/keyboard_shortcuts_sequence.test.tsx.snap b/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/__snapshots__/keyboard_shortcuts_sequence.test.tsx.snap index 5e5571ce52..cfe0c0aca0 100644 --- a/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/__snapshots__/keyboard_shortcuts_sequence.test.tsx.snap +++ b/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/__snapshots__/keyboard_shortcuts_sequence.test.tsx.snap @@ -1,64 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/shortcuts/KeyboardShortcutsSequence should create sequence with order 1`] = ` - -
- - - Ctrl - - - - - Alt - - - - - 3 - - -
-
-`; - exports[`components/shortcuts/KeyboardShortcutsSequence should match snapshot when used for modal with description 1`] = ` { 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( - , - ); - expect(wrapper).toMatchSnapshot(); - const tag = {'Keyboard shortcuts'}; - expect(wrapper.contains(tag)).toEqual(false); - expect(wrapper.find('.shortcut-key--tooltip')).toHaveLength(3); - expect(wrapper.find('.shortcut-key--shortcut-modal')).toHaveLength(0); - }); }); diff --git a/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/keyboard_shortcuts_sequence.tsx b/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/keyboard_shortcuts_sequence.tsx index 1a3d426890..118d4cb9a5 100644 --- a/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/keyboard_shortcuts_sequence.tsx +++ b/webapp/channels/src/components/keyboard_shortcuts/keyboard_shortcuts_sequence/keyboard_shortcuts_sequence.tsx @@ -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>; 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 = ''; diff --git a/webapp/channels/src/components/post_view/post_reaction/__snapshots__/post_reaction.test.tsx.snap b/webapp/channels/src/components/post_view/post_reaction/__snapshots__/post_reaction.test.tsx.snap index d4d371142d..fc9c33520e 100644 --- a/webapp/channels/src/components/post_view/post_reaction/__snapshots__/post_reaction.test.tsx.snap +++ b/webapp/channels/src/components/post_view/post_reaction/__snapshots__/post_reaction.test.tsx.snap @@ -17,27 +17,14 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = ` target={[MockFunction]} topOffset={-7} /> - - - - } + - + `; diff --git a/webapp/channels/src/components/post_view/post_reaction/post_reaction.test.tsx b/webapp/channels/src/components/post_view/post_reaction/post_reaction.test.tsx index 540e3d4db3..f8cc9a77b3 100644 --- a/webapp/channels/src/components/post_view/post_reaction/post_reaction.test.tsx +++ b/webapp/channels/src/components/post_view/post_reaction/post_reaction.test.tsx @@ -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(); 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); diff --git a/webapp/channels/src/components/post_view/post_reaction/post_reaction.tsx b/webapp/channels/src/components/post_view/post_reaction/post_reaction.tsx index 068f5a00a3..303a5b34d6 100644 --- a/webapp/channels/src/components/post_view/post_reaction/post_reaction.tsx +++ b/webapp/channels/src/components/post_view/post_reaction/post_reaction.tsx @@ -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 { 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 { spaceRequiredAbove={spaceRequiredAbove} spaceRequiredBelow={spaceRequiredBelow} /> - - - - } > - + ); diff --git a/webapp/channels/src/components/post_view/post_recent_reactions/post_recent_reactions.tsx b/webapp/channels/src/components/post_view/post_recent_reactions/post_recent_reactions.tsx index 1a6c844b8f..d2abac7ded 100644 --- a/webapp/channels/src/components/post_view/post_recent_reactions/post_recent_reactions.tsx +++ b/webapp/channels/src/components/post_view/post_recent_reactions/post_recent_reactions.tsx @@ -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 { - 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 - -
- -
- {this.emojiName(emoji, this.props.locale)} - - } >
@@ -121,7 +107,7 @@ export default class PostRecentReactions extends React.PureComponent
-
+ ), ); diff --git a/webapp/channels/src/components/post_view/post_recent_reactions/recent_reactions_emoji_item.tsx b/webapp/channels/src/components/post_view/post_recent_reactions/recent_reactions_emoji_item.tsx index e97b2ae087..b0190eebc1 100644 --- a/webapp/channels/src/components/post_view/post_recent_reactions/recent_reactions_emoji_item.tsx +++ b/webapp/channels/src/components/post_view/post_recent_reactions/recent_reactions_emoji_item.tsx @@ -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 (
- - } - 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", - }, - ] - } - /> - - } - placement="top" - shouldUpdatePosition={true} - trigger={ + @@ -87,54 +64,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does - + `; exports[`components/post_view/Reaction should apply read-only class if user does not have permission to remove reaction 1`] = ` - - - } - 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", - }, - ] - } - /> - - } - placement="top" - shouldUpdatePosition={true} - trigger={ + @@ -177,54 +131,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does - + `; exports[`components/post_view/Reaction should match snapshot 1`] = ` - - - } - 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", - }, - ] - } - /> - - } - placement="top" - shouldUpdatePosition={true} - trigger={ + @@ -267,54 +198,31 @@ exports[`components/post_view/Reaction should match snapshot 1`] = ` - + `; exports[`components/post_view/Reaction should match snapshot when a current user reacted to a post 1`] = ` - - - } - 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", - }, - ] - } - /> - - } - placement="top" - shouldUpdatePosition={true} - trigger={ + @@ -357,7 +265,7 @@ exports[`components/post_view/Reaction should match snapshot when a current user - + `; exports[`components/post_view/Reaction should return null/empty if no emojiImageUrl 1`] = `""`; diff --git a/webapp/channels/src/components/post_view/reaction/reaction.scss b/webapp/channels/src/components/post_view/reaction/reaction.scss index 15b1c911aa..bf19cf21d9 100644 --- a/webapp/channels/src/components/post_view/reaction/reaction.scss +++ b/webapp/channels/src/components/post_view/reaction/reaction.scss @@ -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 { diff --git a/webapp/channels/src/components/post_view/reaction/reaction.tsx b/webapp/channels/src/components/post_view/reaction/reaction.tsx index 45e69a557e..1ec3fb4ef8 100644 --- a/webapp/channels/src/components/post_view/reaction/reaction.tsx +++ b/webapp/channels/src/components/post_view/reaction/reaction.tsx @@ -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 { ); return ( - - - - } - onEnter={this.loadMissingProfiles} + - + ); } } diff --git a/webapp/channels/src/components/post_view/reaction/reaction_tooltip/reaction_tooltip.tsx b/webapp/channels/src/components/post_view/reaction/reaction_tooltip/reaction_tooltip.tsx index 19fdb91150..90b38399ca 100644 --- a/webapp/channels/src/components/post_view/reaction/reaction_tooltip/reaction_tooltip.tsx +++ b/webapp/channels/src/components/post_view/reaction/reaction_tooltip/reaction_tooltip.tsx @@ -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) => { 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 = ( - + 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 = ( - + names = intl.formatMessage( + { + id: 'reaction.othersReacted', + defaultMessage: '{otherUsers, number} {otherUsers, plural, one {user} other {users}}', + }, + { + otherUsers: otherUsersCount, + }, ); } } else if (users.length > 1) { - names = ( - + 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 = ( - - ); + reactionVerb = intl.formatMessage({ + id: 'reaction.reactionVerb.youAndUsers', + defaultMessage: 'reacted', + }); } else { - reactionVerb = ( - - ); + reactionVerb = intl.formatMessage({ + id: 'reaction.reactionVerb.users', + defaultMessage: 'reacted', + }); } } else if (currentUserReacted) { - reactionVerb = ( - - ); + reactionVerb = intl.formatMessage({ + id: 'reaction.reactionVerb.you', + defaultMessage: 'reacted', + }); } else { - reactionVerb = ( - - ); + reactionVerb = intl.formatMessage({ + id: 'reaction.reactionVerb.user', + defaultMessage: 'reacted', + }); } - const tooltip = ( - {names}, - reactionVerb, - emoji: {':' + emojiName + ':'}, - }} - /> + 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 = ( - - ); + clickTooltip = intl.formatMessage({ + id: 'reaction.clickToRemove', + defaultMessage: '(click to remove)', + }); } else if (!currentUserReacted && canAddReactions) { - clickTooltip = ( - - ); + clickTooltip = intl.formatMessage({ + id: 'reaction.clickToAdd', + defaultMessage: '(click to add)', + }); } return ( - <> -
{emojiIcon}
- {tooltip} -
- {clickTooltip} - + + {children} + ); }; diff --git a/webapp/channels/src/components/post_view/reaction_list/__snapshots__/reactions_list.test.tsx.snap b/webapp/channels/src/components/post_view/reaction_list/__snapshots__/reactions_list.test.tsx.snap index fe71ea5d0e..640168e29e 100644 --- a/webapp/channels/src/components/post_view/reaction_list/__snapshots__/reactions_list.test.tsx.snap +++ b/webapp/channels/src/components/post_view/reaction_list/__snapshots__/reactions_list.test.tsx.snap @@ -71,25 +71,14 @@ exports[`components/ReactionList should render when there are reactions 1`] = ` } teamId="teamId" > - - - - } + - +
diff --git a/webapp/channels/src/components/post_view/reaction_list/reaction_list.tsx b/webapp/channels/src/components/post_view/reaction_list/reaction_list.tsx index b52e44ceb5..6cdffcbdcb 100644 --- a/webapp/channels/src/components/post_view/reaction_list/reaction_list.tsx +++ b/webapp/channels/src/components/post_view/reaction_list/reaction_list.tsx @@ -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 { 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 { let emojiPicker = null; if (this.props.canAddReactions) { - const addReactionTooltip = ( - - - - ); - emojiPicker = ( { teamId={this.props.teamId} permissions={[Permissions.ADD_REACTION]} > - - + ); diff --git a/webapp/channels/src/components/purchase_modal/purchase.scss b/webapp/channels/src/components/purchase_modal/purchase.scss index 38fc5d429b..044bcfe3e3 100644 --- a/webapp/channels/src/components/purchase_modal/purchase.scss +++ b/webapp/channels/src/components/purchase_modal/purchase.scss @@ -545,16 +545,3 @@ right: 12px; } } - -.tooltipTitle { - font-weight: 700; -} - -.tooltipText { - color: #9a9a9a !important; - text-align: left; -} - -.proratedTooltip > .tooltip-inner { - min-width: 300px; -} diff --git a/webapp/channels/src/components/purchase_modal/renewal_card.tsx b/webapp/channels/src/components/purchase_modal/renewal_card.tsx index d5f945fc99..0d635f79c4 100644 --- a/webapp/channels/src/components/purchase_modal/renewal_card.tsx +++ b/webapp/channels/src/components/purchase_modal/renewal_card.tsx @@ -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' /> - -
- -
-
- -
- - } > -
+
{partialCharges.map((charge: any) => (
-
- -
-
- -
- - ); return (
@@ -198,16 +189,17 @@ export default function SeatsCalculator(props: Props) {
- - +
@@ -242,6 +234,5 @@ export default function SeatsCalculator(props: Props) { )} - ); } diff --git a/webapp/channels/src/components/sidebar/channel_filter/__snapshots__/channel_filter.test.tsx.snap b/webapp/channels/src/components/sidebar/channel_filter/__snapshots__/channel_filter.test.tsx.snap index 71d9c9ec38..e2bb76718c 100644 --- a/webapp/channels/src/components/sidebar/channel_filter/__snapshots__/channel_filter.test.tsx.snap +++ b/webapp/channels/src/components/sidebar/channel_filter/__snapshots__/channel_filter.test.tsx.snap @@ -4,39 +4,37 @@ exports[`components/sidebar/channel_filter should match snapshot 1`] = `
- - Filter by unread - - - } + - +
`; @@ -57,39 +55,37 @@ exports[`components/sidebar/channel_filter should match snapshot if the unread f
- - Show all channels - - - } + - +
`; diff --git a/webapp/channels/src/components/sidebar/channel_filter/channel_filter.tsx b/webapp/channels/src/components/sidebar/channel_filter/channel_filter.tsx index 012b2bc1cd..f7a9f7d5f7 100644 --- a/webapp/channels/src/components/sidebar/channel_filter/channel_filter.tsx +++ b/webapp/channels/src/components/sidebar/channel_filter/channel_filter.tsx @@ -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 { 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 = ( - - {tooltipMessage} - - - ); - return (
- { > - +
); } diff --git a/webapp/channels/src/components/team_sidebar/components/team_button.tsx b/webapp/channels/src/components/team_sidebar/components/team_button.tsx index c139cf0199..ac3ff52ce0 100644 --- a/webapp/channels/src/components/team_sidebar/components/team_button.tsx +++ b/webapp/channels/src/components/team_sidebar/components/team_button.tsx @@ -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 { - 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 = ( + + ); + } 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 = ( - - ); - } 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 = ( - {mentions > 99 ? '99+' : mentions} - ); - } - } - - ariaLabel = ariaLabel.toLowerCase(); - - const content = ( - - ); - - 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} - - + badge = ( + {mentions > 99 ? '99+' : mentions} ); + } + } - if (this.props.showOrder) { - orderIndicator = ( -
- {order} + ariaLabel = ariaLabel.toLowerCase(); + + const content = ( + + ); + + let orderIndicator: JSX.Element | undefined; + if (typeof order !== 'undefined' && order < 10) { + if (otherProps.showOrder) { + orderIndicator = ( +
+ {order} +
+ ); + } + } + + const btn = ( + +
+ {!otherProps.isInProduct && badge} + {content} +
+
+ ); + + let teamButton = ( + + {btn} + + ); + + if (isDesktopApp()) { + // if this is not a "special" team button, give it a context menu + if (isNotCreateTeamButton) { + teamButton = ( + + {teamButton} + + ); + } + } + + return isDraggable ? ( + + {(provided, snapshot) => { + return ( +
+
+ {teamButton} + {orderIndicator} +
); - } - } - - const btn = ( - - {toolTip} - - } - > -
- {!this.props.isInProduct && badge} - {content} -
-
- ); - - let teamButton = ( - - {btn} - - ); - - if (isDesktopApp()) { - // if this is not a "special" team button, give it a context menu - if (isNotCreateTeamButton) { - teamButton = ( - - {teamButton} - - ); - } - } - - return isDraggable ? ( - - {(provided, snapshot) => { - return ( -
-
- {teamButton} - {orderIndicator} -
-
- ); - }} -
- ) : ( -
- {teamButton} - {orderIndicator} -
- ); - } + }} +
+ ) : ( +
+ {teamButton} + {orderIndicator} +
+ ); } -export default injectIntl(TeamButton); +function WithTeamTooltip({ + order, + tip, + url, + children, +}: React.PropsWithChildren>) { + 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 ( + + {children} + + ); +} diff --git a/webapp/channels/src/components/with_tooltip/create_tooltip.tsx b/webapp/channels/src/components/with_tooltip/create_tooltip.tsx index 2d1ccfa4af..7446f95b45 100644 --- a/webapp/channels/src/components/with_tooltip/create_tooltip.tsx +++ b/webapp/channels/src/components/with_tooltip/create_tooltip.tsx @@ -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, 'children' | 'id'>) => { - const title = getStringOrDescriptorComponent(commonTooltipProps.title); - const hint = getStringOrDescriptorComponent(commonTooltipProps.hint); + const contents = []; + + if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle === 'large') { + contents.push( +
+ +
, + ); + } + + const title = getAsFormattedMessage(commonTooltipProps.title); + if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle !== 'large') { + contents.push( +
+ + {title} +
, + ); + } else { + contents.push( +
+ {title} +
, + ); + } + + if (commonTooltipProps.shortcut) { + contents.push( +
+ +
, + ); + } + + const hint = getAsFormattedMessage(commonTooltipProps.hint); + if (commonTooltipProps.hint) { + contents.push( +
+ {hint} +
, + ); + } - const emoji = commonTooltipProps.emoji && ( - - ); return ( -
- {emoji} - {title} -
- {commonTooltipProps.shortcut && ( -
- {commonTooltipProps.shortcut.map((v) => ( - - {v} - - ))} -
- )} - {commonTooltipProps.hint && (
{hint}
)} + {contents}
); }; diff --git a/webapp/channels/src/components/with_tooltip/index.tsx b/webapp/channels/src/components/with_tooltip/index.tsx index 3728c547e9..25bda6089e 100644 --- a/webapp/channels/src/components/with_tooltip/index.tsx +++ b/webapp/channels/src/components/with_tooltip/index.tsx @@ -16,29 +16,34 @@ type OverlayTriggerProps = ComponentProps; 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 ( } placement={placement} + onEnter={onShow} > {children} diff --git a/webapp/channels/src/components/with_tooltip/shortcut.tsx b/webapp/channels/src/components/with_tooltip/shortcut.tsx new file mode 100644 index 0000000000..4e628f401f --- /dev/null +++ b/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 = ; + } else { + key = v; + content = v; + } + + return ( + + {content} + + ); + })} + + ); +} diff --git a/webapp/channels/src/components/with_tooltip/utils.tsx b/webapp/channels/src/components/with_tooltip/utils.tsx index bc90654e8d..1a2998674d 100644 --- a/webapp/channels/src/components/with_tooltip/utils.tsx +++ b/webapp/channels/src/components/with_tooltip/utils.tsx @@ -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['values']) { +import {isMessageDescriptor} from 'utils/i18n'; + +export function getAsFormattedMessage(v: string | MessageDescriptor | React.ReactElement | undefined, values?: ComponentProps['values']) { if (!v) { return undefined; } - if (typeof v === 'string') { - return v; + if (isMessageDescriptor(v)) { + return ( + + ); } - return ( - - ); + return v; } diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 7032c408ef..3e0a7efaa8 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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.", diff --git a/webapp/channels/src/packages/mattermost-redux/src/utils/emoji_utils.ts b/webapp/channels/src/packages/mattermost-redux/src/utils/emoji_utils.ts index dbbeabbc75..25d1c87c51 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/utils/emoji_utils.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/utils/emoji_utils.ts @@ -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 []; diff --git a/webapp/channels/src/sass/components/_tooltip.scss b/webapp/channels/src/sass/components/_tooltip.scss index c3ddf16509..7a7ef8227e 100644 --- a/webapp/channels/src/sass/components/_tooltip.scss +++ b/webapp/channels/src/sass/components/_tooltip.scss @@ -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; } } diff --git a/webapp/channels/src/utils/i18n.tsx b/webapp/channels/src/utils/i18n.tsx index 90d082ea4b..1e62b93291 100644 --- a/webapp/channels/src/utils/i18n.tsx +++ b/webapp/channels/src/utils/i18n.tsx @@ -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