Simplifying the channel_header component (#25795)
* Simplifying the channel_header component * Addressing PR Review Comments * Addressing PR Review Comments * Addressing linter errors * Fixing CI * Fixing CI * Addressing PR Review Comments
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
93351ff8a4
Коммит
e959d9e4ea
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -10,7 +10,6 @@ import type {UserCustomStatus} from '@mattermost/types/users';
|
|||||||
import ChannelHeader from 'components/channel_header/channel_header';
|
import ChannelHeader from 'components/channel_header/channel_header';
|
||||||
import ChannelInfoButton from 'components/channel_header/channel_info_button';
|
import ChannelInfoButton from 'components/channel_header/channel_info_button';
|
||||||
import Markdown from 'components/markdown';
|
import Markdown from 'components/markdown';
|
||||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
|
||||||
|
|
||||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
import Constants, {RHSStates} from 'utils/constants';
|
import Constants, {RHSStates} from 'utils/constants';
|
||||||
@@ -19,8 +18,6 @@ import {TestHelper} from 'utils/test_helper';
|
|||||||
describe('components/ChannelHeader', () => {
|
describe('components/ChannelHeader', () => {
|
||||||
const baseProps: ComponentProps<typeof ChannelHeader> = {
|
const baseProps: ComponentProps<typeof ChannelHeader> = {
|
||||||
actions: {
|
actions: {
|
||||||
favoriteChannel: jest.fn(),
|
|
||||||
unfavoriteChannel: jest.fn(),
|
|
||||||
showPinnedPosts: jest.fn(),
|
showPinnedPosts: jest.fn(),
|
||||||
showChannelFiles: jest.fn(),
|
showChannelFiles: jest.fn(),
|
||||||
closeRightHandSide: jest.fn(),
|
closeRightHandSide: jest.fn(),
|
||||||
@@ -36,7 +33,6 @@ describe('components/ChannelHeader', () => {
|
|||||||
channel: TestHelper.getChannelMock({}),
|
channel: TestHelper.getChannelMock({}),
|
||||||
channelMember: TestHelper.getChannelMembershipMock({}),
|
channelMember: TestHelper.getChannelMembershipMock({}),
|
||||||
currentUser: TestHelper.getUserMock({}),
|
currentUser: TestHelper.getUserMock({}),
|
||||||
teammateNameDisplaySetting: '',
|
|
||||||
currentRelativeTeamUrl: '',
|
currentRelativeTeamUrl: '',
|
||||||
isCustomStatusEnabled: false,
|
isCustomStatusEnabled: false,
|
||||||
isCustomStatusExpired: false,
|
isCustomStatusExpired: false,
|
||||||
@@ -267,36 +263,6 @@ describe('components/ChannelHeader', () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render the guest tags on gms', () => {
|
|
||||||
const props = {
|
|
||||||
...populatedProps,
|
|
||||||
channel: TestHelper.getChannelMock({
|
|
||||||
header: 'test',
|
|
||||||
display_name: 'regular_user, guest_user',
|
|
||||||
type: Constants.GM_CHANNEL as ChannelType,
|
|
||||||
}),
|
|
||||||
gmMembers: [
|
|
||||||
TestHelper.getUserMock({
|
|
||||||
id: 'user_id',
|
|
||||||
username: 'regular_user',
|
|
||||||
roles: 'system_user',
|
|
||||||
}),
|
|
||||||
TestHelper.getUserMock({
|
|
||||||
id: 'guest_id',
|
|
||||||
username: 'guest_user',
|
|
||||||
roles: 'system_guest',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallowWithIntl(
|
|
||||||
<ChannelHeader {...props}/>,
|
|
||||||
);
|
|
||||||
expect(wrapper.containsMatchingElement(
|
|
||||||
<GuestTag/>,
|
|
||||||
)).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render properly when custom status is set', () => {
|
test('should render properly when custom status is set', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...populatedProps,
|
...populatedProps,
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import type {UserCustomStatus, UserProfile} from '@mattermost/types/users';
|
|||||||
|
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
||||||
import {displayUsername, isGuest} from 'mattermost-redux/utils/user_utils';
|
|
||||||
|
|
||||||
import {ChannelHeaderDropdown} from 'components/channel_header_dropdown';
|
|
||||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||||
import CustomStatusText from 'components/custom_status/custom_status_text';
|
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||||
@@ -23,15 +21,10 @@ import Markdown from 'components/markdown';
|
|||||||
import OverlayTrigger from 'components/overlay_trigger';
|
import OverlayTrigger from 'components/overlay_trigger';
|
||||||
import type {BaseOverlayTrigger} from 'components/overlay_trigger';
|
import type {BaseOverlayTrigger} from 'components/overlay_trigger';
|
||||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||||
import SharedChannelIndicator from 'components/shared_channel_indicator';
|
|
||||||
import StatusIcon from 'components/status_icon';
|
import StatusIcon from 'components/status_icon';
|
||||||
import Timestamp from 'components/timestamp';
|
import Timestamp from 'components/timestamp';
|
||||||
import Tooltip from 'components/tooltip';
|
import Tooltip from 'components/tooltip';
|
||||||
import ArchiveIcon from 'components/widgets/icons/archive_icon';
|
|
||||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
|
||||||
import Popover from 'components/widgets/popover';
|
import Popover from 'components/widgets/popover';
|
||||||
import BotTag from 'components/widgets/tag/bot_tag';
|
|
||||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
|
||||||
|
|
||||||
import CallButton from 'plugins/call_button';
|
import CallButton from 'plugins/call_button';
|
||||||
import ChannelHeaderPlug from 'plugins/channel_header_plug';
|
import ChannelHeaderPlug from 'plugins/channel_header_plug';
|
||||||
@@ -46,6 +39,7 @@ import {handleFormattedTextClick, localizeMessage, isEmptyObject, toTitleCase} f
|
|||||||
import type {ModalData} from 'types/actions';
|
import type {ModalData} from 'types/actions';
|
||||||
import type {RhsState} from 'types/store/rhs';
|
import type {RhsState} from 'types/store/rhs';
|
||||||
|
|
||||||
|
import ChannelHeaderTitle from './channel_header_title';
|
||||||
import ChannelInfoButton from './channel_info_button';
|
import ChannelInfoButton from './channel_info_button';
|
||||||
import HeaderIconWrapper from './components/header_icon_wrapper';
|
import HeaderIconWrapper from './components/header_icon_wrapper';
|
||||||
|
|
||||||
@@ -60,7 +54,6 @@ export type Props = {
|
|||||||
channelMember?: ChannelMembership;
|
channelMember?: ChannelMembership;
|
||||||
dmUser?: UserProfile;
|
dmUser?: UserProfile;
|
||||||
gmMembers?: UserProfile[];
|
gmMembers?: UserProfile[];
|
||||||
isFavorite?: boolean;
|
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
hasGuests?: boolean;
|
hasGuests?: boolean;
|
||||||
@@ -71,8 +64,6 @@ export type Props = {
|
|||||||
pinnedPostsCount?: number;
|
pinnedPostsCount?: number;
|
||||||
hasMoreThanOneTeam?: boolean;
|
hasMoreThanOneTeam?: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
favoriteChannel: (channelId: string) => void;
|
|
||||||
unfavoriteChannel: (channelId: string) => void;
|
|
||||||
showPinnedPosts: (channelId?: string) => void;
|
showPinnedPosts: (channelId?: string) => void;
|
||||||
showChannelFiles: (channelId: string) => void;
|
showChannelFiles: (channelId: string) => void;
|
||||||
closeRightHandSide: () => void;
|
closeRightHandSide: () => void;
|
||||||
@@ -83,7 +74,6 @@ export type Props = {
|
|||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
showChannelMembers: (channelId: string, inEditingMode?: boolean) => void;
|
showChannelMembers: (channelId: string, inEditingMode?: boolean) => void;
|
||||||
};
|
};
|
||||||
teammateNameDisplaySetting: string;
|
|
||||||
currentRelativeTeamUrl: string;
|
currentRelativeTeamUrl: string;
|
||||||
announcementBarCount: number;
|
announcementBarCount: number;
|
||||||
customStatus?: UserCustomStatus;
|
customStatus?: UserCustomStatus;
|
||||||
@@ -97,7 +87,6 @@ export type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
titleMenuOpen: boolean;
|
|
||||||
showChannelHeaderPopover: boolean;
|
showChannelHeaderPopover: boolean;
|
||||||
channelHeaderPoverWidth: number;
|
channelHeaderPoverWidth: number;
|
||||||
leftOffset: number;
|
leftOffset: number;
|
||||||
@@ -124,7 +113,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
channelHeaderPoverWidth: 0,
|
channelHeaderPoverWidth: 0,
|
||||||
leftOffset: 0,
|
leftOffset: 0,
|
||||||
topOffset: 0,
|
topOffset: 0,
|
||||||
titleMenuOpen: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getHeaderMarkdownOptions = memoizeResult((channelNamesMap: Record<string, any>) => (
|
this.getHeaderMarkdownOptions = memoizeResult((channelNamesMap: Record<string, any>) => (
|
||||||
@@ -149,15 +137,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
handleClose = () => this.props.actions.goToLastViewedChannel();
|
handleClose = () => this.props.actions.goToLastViewedChannel();
|
||||||
|
|
||||||
toggleFavorite = (e: MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (this.props.isFavorite) {
|
|
||||||
this.props.actions.unfavoriteChannel(this.props.channel.id);
|
|
||||||
} else {
|
|
||||||
this.props.actions.favoriteChannel(this.props.channel.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
unmute = () => {
|
unmute = () => {
|
||||||
const {actions, channel, channelMember, currentUser} = this.props;
|
const {actions, channel, channelMember, currentUser} = this.props;
|
||||||
|
|
||||||
@@ -197,13 +176,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
removeTooltipLink = () => {
|
|
||||||
// Bootstrap adds the attr dynamically, removing it to prevent a11y readout
|
|
||||||
this.toggleFavoriteRef.current?.removeAttribute('aria-describedby');
|
|
||||||
};
|
|
||||||
|
|
||||||
setTitleMenuOpen = (open: boolean) => this.setState({titleMenuOpen: open});
|
|
||||||
|
|
||||||
showEditChannelHeaderModal = () => {
|
showEditChannelHeaderModal = () => {
|
||||||
if (this.headerOverlayRef.current) {
|
if (this.headerOverlayRef.current) {
|
||||||
this.headerOverlayRef.current.hide();
|
this.headerOverlayRef.current.hide();
|
||||||
@@ -283,11 +255,9 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
channelMember,
|
channelMember,
|
||||||
isMuted: channelMuted,
|
isMuted: channelMuted,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
isFavorite,
|
|
||||||
dmUser,
|
dmUser,
|
||||||
rhsState,
|
rhsState,
|
||||||
hasGuests,
|
hasGuests,
|
||||||
teammateNameDisplaySetting,
|
|
||||||
hideGuestTags,
|
hideGuestTags,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
@@ -321,81 +291,11 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
const channelNamesMap = channel.props && channel.props.channel_mentions;
|
const channelNamesMap = channel.props && channel.props.channel_mentions;
|
||||||
|
|
||||||
let channelTitle: ReactNode = channel.display_name;
|
|
||||||
const archivedIcon = channelIsArchived ? <ArchiveIcon className='icon icon__archive icon channel-header-archived-icon svg-text-color'/> : null;
|
|
||||||
let sharedIcon = null;
|
|
||||||
if (channel.shared) {
|
|
||||||
sharedIcon = (
|
|
||||||
<SharedChannelIndicator
|
|
||||||
className='shared-channel-icon'
|
|
||||||
channelType={channel.type}
|
|
||||||
withTooltip={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const isDirect = (channel.type === Constants.DM_CHANNEL);
|
const isDirect = (channel.type === Constants.DM_CHANNEL);
|
||||||
const isGroup = (channel.type === Constants.GM_CHANNEL);
|
const isGroup = (channel.type === Constants.GM_CHANNEL);
|
||||||
const isPrivate = (channel.type === Constants.PRIVATE_CHANNEL);
|
const isPrivate = (channel.type === Constants.PRIVATE_CHANNEL);
|
||||||
|
|
||||||
if (isDirect) {
|
|
||||||
const teammateId = dmUser?.id;
|
|
||||||
if (currentUser.id === teammateId) {
|
|
||||||
channelTitle = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='channel_header.directchannel.you'
|
|
||||||
defaultMessage='{displayname} (you) '
|
|
||||||
values={{
|
|
||||||
displayname: displayUsername(dmUser, teammateNameDisplaySetting),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
channelTitle = displayUsername(dmUser, teammateNameDisplaySetting) + ' ';
|
|
||||||
}
|
|
||||||
channelTitle = (
|
|
||||||
<React.Fragment>
|
|
||||||
{channelTitle}
|
|
||||||
{isGuest(dmUser?.roles ?? '') && <GuestTag/>}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
// map the displayname to the gm member users
|
|
||||||
const membersMap: Record<string, UserProfile[]> = {};
|
|
||||||
if (gmMembers) {
|
|
||||||
for (const user of gmMembers) {
|
|
||||||
if (user.id === currentUser.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const userDisplayName = displayUsername(user, this.props.teammateNameDisplaySetting);
|
|
||||||
|
|
||||||
if (!membersMap[userDisplayName]) {
|
|
||||||
membersMap[userDisplayName] = []; //Create an array for cases with same display name
|
|
||||||
}
|
|
||||||
|
|
||||||
membersMap[userDisplayName].push(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayNames = channel.display_name.split(', ');
|
|
||||||
|
|
||||||
channelTitle = displayNames.map((displayName, index) => {
|
|
||||||
if (!membersMap[displayName]) {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = membersMap[displayName].shift();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment key={user?.id}>
|
|
||||||
{index > 0 && ', '}
|
|
||||||
{displayName}
|
|
||||||
{isGuest(user?.roles ?? '') && <GuestTag/>}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasGuests && !hideGuestTags) {
|
if (hasGuests && !hideGuestTags) {
|
||||||
hasGuestsText = (
|
hasGuestsText = (
|
||||||
<span className='has-guest-header'>
|
<span className='has-guest-header'>
|
||||||
@@ -690,49 +590,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let toggleFavoriteTooltip;
|
|
||||||
let toggleFavorite = null;
|
|
||||||
let ariaLabel = '';
|
|
||||||
|
|
||||||
if (!channelIsArchived) {
|
|
||||||
const formattedMessage = isFavorite ? {
|
|
||||||
id: 'channelHeader.removeFromFavorites',
|
|
||||||
defaultMessage: 'Remove from Favorites',
|
|
||||||
} : {
|
|
||||||
id: 'channelHeader.addToFavorites',
|
|
||||||
defaultMessage: 'Add to Favorites',
|
|
||||||
};
|
|
||||||
|
|
||||||
ariaLabel = formatMessage(formattedMessage).toLowerCase();
|
|
||||||
toggleFavoriteTooltip = (
|
|
||||||
<Tooltip id='favoriteTooltip' >
|
|
||||||
<FormattedMessage
|
|
||||||
{...formattedMessage}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
toggleFavorite = (
|
|
||||||
<OverlayTrigger
|
|
||||||
key={`isFavorite-${isFavorite}`}
|
|
||||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
|
||||||
placement='bottom'
|
|
||||||
overlay={toggleFavoriteTooltip}
|
|
||||||
onEntering={this.removeTooltipLink}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
id='toggleFavorite'
|
|
||||||
ref={this.toggleFavoriteRef}
|
|
||||||
onClick={this.toggleFavorite}
|
|
||||||
className={'style--none color--link channel-header__favorites ' + (this.props.isFavorite ? 'active' : 'inactive')}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
>
|
|
||||||
<i className={'icon ' + (this.props.isFavorite ? 'icon-star' : 'icon-star-outline')}/>
|
|
||||||
</button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelMutedTooltip = (
|
const channelMutedTooltip = (
|
||||||
<Tooltip id='channelMutedTooltip'>
|
<Tooltip id='channelMutedTooltip'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -762,63 +619,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = (
|
|
||||||
<React.Fragment>
|
|
||||||
<MenuWrapper onToggle={this.setTitleMenuOpen}>
|
|
||||||
<div
|
|
||||||
id='channelHeaderDropdownButton'
|
|
||||||
className='channel-header__top'
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className={`channel-header__trigger style--none ${this.state.titleMenuOpen ? 'active' : ''}`}
|
|
||||||
aria-label={formatMessage({id: 'channel_header.menuAriaLabel', defaultMessage: 'Channel Menu'}).toLowerCase()}
|
|
||||||
>
|
|
||||||
<strong
|
|
||||||
role='heading'
|
|
||||||
aria-level={2}
|
|
||||||
id='channelHeaderTitle'
|
|
||||||
className='heading'
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{archivedIcon}
|
|
||||||
{channelTitle}
|
|
||||||
{sharedIcon}
|
|
||||||
</span>
|
|
||||||
</strong>
|
|
||||||
<span
|
|
||||||
id='channelHeaderDropdownIcon'
|
|
||||||
className='icon icon-chevron-down header-dropdown-chevron-icon'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ChannelHeaderDropdown/>
|
|
||||||
</MenuWrapper>
|
|
||||||
{toggleFavorite}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
if (isDirect && dmUser?.is_bot) {
|
|
||||||
title = (
|
|
||||||
<div
|
|
||||||
id='channelHeaderDropdownButton'
|
|
||||||
className='channel-header__top channel-header__bot'
|
|
||||||
>
|
|
||||||
<strong
|
|
||||||
role='heading'
|
|
||||||
aria-level={2}
|
|
||||||
id='channelHeaderTitle'
|
|
||||||
className='heading'
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{archivedIcon}
|
|
||||||
{channelTitle}
|
|
||||||
</span>
|
|
||||||
</strong>
|
|
||||||
<BotTag/>
|
|
||||||
{toggleFavorite}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id='channel-header'
|
id='channel-header'
|
||||||
@@ -839,7 +639,10 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
className='channel-header__title dropdown'
|
className='channel-header__title dropdown'
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{title}
|
<ChannelHeaderTitle
|
||||||
|
dmUser={dmUser}
|
||||||
|
gmMembers={gmMembers}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{muteTrigger}
|
{muteTrigger}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import React, {memo, useState} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
|
||||||
|
import {ChannelHeaderDropdown} from 'components/channel_header_dropdown';
|
||||||
|
import SharedChannelIndicator from 'components/shared_channel_indicator';
|
||||||
|
import ArchiveIcon from 'components/widgets/icons/archive_icon';
|
||||||
|
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||||
|
import BotTag from 'components/widgets/tag/bot_tag';
|
||||||
|
|
||||||
|
import {Constants} from 'utils/constants';
|
||||||
|
|
||||||
|
import ChannelHeaderTitleDirect from './channel_header_title_direct';
|
||||||
|
import ChannelHeaderTitleFavorite from './channel_header_title_favorite';
|
||||||
|
import ChannelHeaderTitleGroup from './channel_header_title_group';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dmUser?: UserProfile;
|
||||||
|
gmMembers?: UserProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChannelHeaderTitle = ({
|
||||||
|
dmUser,
|
||||||
|
gmMembers,
|
||||||
|
}: Props) => {
|
||||||
|
const [titleMenuOpen, setTitleMenuOpen] = useState(false);
|
||||||
|
const intl = useIntl();
|
||||||
|
const channel = useSelector(getCurrentChannel);
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirect = (channel.type === Constants.DM_CHANNEL);
|
||||||
|
const isGroup = (channel.type === Constants.GM_CHANNEL);
|
||||||
|
const channelIsArchived = channel.delete_at !== 0;
|
||||||
|
|
||||||
|
let archivedIcon: React.ReactNode = null;
|
||||||
|
if (channelIsArchived) {
|
||||||
|
archivedIcon = <ArchiveIcon className='icon icon__archive icon channel-header-archived-icon svg-text-color'/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sharedIcon = null;
|
||||||
|
if (channel.shared) {
|
||||||
|
sharedIcon = (
|
||||||
|
<SharedChannelIndicator
|
||||||
|
className='shared-channel-icon'
|
||||||
|
channelType={channel.type}
|
||||||
|
withTooltip={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let channelTitle: ReactNode = channel.display_name;
|
||||||
|
if (isDirect) {
|
||||||
|
channelTitle = <ChannelHeaderTitleDirect dmUser={dmUser}/>;
|
||||||
|
} else if (isGroup) {
|
||||||
|
channelTitle = <ChannelHeaderTitleGroup gmMembers={gmMembers}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirect && dmUser?.is_bot) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id='channelHeaderDropdownButton'
|
||||||
|
className='channel-header__top channel-header__bot'
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
role='heading'
|
||||||
|
aria-level={2}
|
||||||
|
id='channelHeaderTitle'
|
||||||
|
className='heading'
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{archivedIcon}
|
||||||
|
{channelTitle}
|
||||||
|
</span>
|
||||||
|
</strong>
|
||||||
|
<BotTag/>
|
||||||
|
<ChannelHeaderTitleFavorite/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<MenuWrapper onToggle={setTitleMenuOpen}>
|
||||||
|
<div
|
||||||
|
id='channelHeaderDropdownButton'
|
||||||
|
className='channel-header__top'
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={classNames('channel-header__trigger style--none', {active: titleMenuOpen})}
|
||||||
|
aria-label={intl.formatMessage({id: 'channel_header.menuAriaLabel', defaultMessage: 'Channel Menu'}).toLowerCase()}
|
||||||
|
>
|
||||||
|
<strong
|
||||||
|
role='heading'
|
||||||
|
aria-level={2}
|
||||||
|
id='channelHeaderTitle'
|
||||||
|
className='heading'
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{archivedIcon}
|
||||||
|
{channelTitle}
|
||||||
|
{sharedIcon}
|
||||||
|
</span>
|
||||||
|
</strong>
|
||||||
|
<span
|
||||||
|
id='channelHeaderDropdownIcon'
|
||||||
|
className='icon icon-chevron-down header-dropdown-chevron-icon'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ChannelHeaderDropdown/>
|
||||||
|
</MenuWrapper>
|
||||||
|
<ChannelHeaderTitleFavorite/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ChannelHeaderTitle);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {displayUsername, isGuest} from 'mattermost-redux/utils/user_utils';
|
||||||
|
|
||||||
|
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dmUser?: UserProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChannelHeaderTitleDirect = ({
|
||||||
|
dmUser,
|
||||||
|
}: Props) => {
|
||||||
|
const currentUser = useSelector(getCurrentUser);
|
||||||
|
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
|
||||||
|
const displayName = displayUsername(dmUser, teammateNameDisplaySetting);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{currentUser.id !== dmUser?.id && displayName + ' '}
|
||||||
|
{currentUser.id === dmUser?.id &&
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.directchannel.you'
|
||||||
|
defaultMessage='{displayName} (you) '
|
||||||
|
values={{displayName}}
|
||||||
|
/>}
|
||||||
|
{isGuest(dmUser?.roles ?? '') && <GuestTag/>}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ChannelHeaderTitleDirect);
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {memo, useRef, useCallback} from 'react';
|
||||||
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
import {useSelector, useDispatch} from 'react-redux';
|
||||||
|
|
||||||
|
import {favoriteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels';
|
||||||
|
import {getCurrentChannel, isCurrentChannelFavorite} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
|
||||||
|
import OverlayTrigger from 'components/overlay_trigger';
|
||||||
|
import Tooltip from 'components/tooltip';
|
||||||
|
|
||||||
|
import {Constants} from 'utils/constants';
|
||||||
|
|
||||||
|
const ChannelHeaderTitleFavorite = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isFavorite = useSelector(isCurrentChannelFavorite);
|
||||||
|
const channel = useSelector(getCurrentChannel);
|
||||||
|
const channelIsArchived = channel.delete_at !== 0;
|
||||||
|
const toggleFavoriteRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const toggleFavoriteCallback = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isFavorite) {
|
||||||
|
dispatch(unfavoriteChannel(channel.id));
|
||||||
|
} else {
|
||||||
|
dispatch(favoriteChannel(channel.id));
|
||||||
|
}
|
||||||
|
}, [isFavorite, channel.id]);
|
||||||
|
|
||||||
|
const removeTooltipLink = useCallback(() => {
|
||||||
|
// Bootstrap adds the attr dynamically, removing it to prevent a11y readout
|
||||||
|
toggleFavoriteRef.current?.removeAttribute('aria-describedby');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!channel || channelIsArchived) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ariaLabel = intl.formatMessage({id: 'channelHeader.addToFavorites', defaultMessage: 'Add to Favorites'});
|
||||||
|
if (isFavorite) {
|
||||||
|
ariaLabel = intl.formatMessage({id: 'channelHeader.removeFromFavorites', defaultMessage: 'Remove from Favorites'});
|
||||||
|
}
|
||||||
|
ariaLabel = ariaLabel.toLowerCase();
|
||||||
|
|
||||||
|
const toggleFavoriteTooltip = (
|
||||||
|
<Tooltip id='favoriteTooltip' >
|
||||||
|
{!isFavorite &&
|
||||||
|
<FormattedMessage
|
||||||
|
id='channelHeader.addToFavorites'
|
||||||
|
defaultMessage='Add to Favorites'
|
||||||
|
/>}
|
||||||
|
{isFavorite &&
|
||||||
|
<FormattedMessage
|
||||||
|
id='channelHeader.removeFromFavorites'
|
||||||
|
defaultMessage='Remove from Favorites'
|
||||||
|
/>}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
key={`isFavorite-${isFavorite}`}
|
||||||
|
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||||
|
placement='bottom'
|
||||||
|
overlay={toggleFavoriteTooltip}
|
||||||
|
onEntering={removeTooltipLink}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id='toggleFavorite'
|
||||||
|
ref={toggleFavoriteRef}
|
||||||
|
onClick={toggleFavoriteCallback}
|
||||||
|
className={classNames('style--none color--link channel-header__favorites', {active: isFavorite, inactive: !isFavorite})}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
<i className={classNames('icon', {'icon-star': isFavorite, 'icon-star-outline': !isFavorite})}/>
|
||||||
|
</button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ChannelHeaderTitleFavorite);
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ChannelHeaderTitleGroup from 'components/channel_header/channel_header_title_group';
|
||||||
|
|
||||||
|
import {renderWithContext} from 'tests/react_testing_utils';
|
||||||
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
|
describe('components/ChannelHeaderTitleGroup', () => {
|
||||||
|
const channels = {
|
||||||
|
channels: {
|
||||||
|
channel_id: {
|
||||||
|
id: 'channel_id',
|
||||||
|
display_name: 'regular_user, guest_user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentChannelId: 'channel_id',
|
||||||
|
};
|
||||||
|
|
||||||
|
const channelsWithoutGuests = {
|
||||||
|
channels: {
|
||||||
|
channel_id: {
|
||||||
|
id: 'channel_id',
|
||||||
|
display_name: 'regular_user, not_guest_user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentChannelId: 'channel_id',
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = {
|
||||||
|
profiles: {
|
||||||
|
user_id: {
|
||||||
|
id: 'user_id',
|
||||||
|
username: 'regular_user',
|
||||||
|
roles: 'system_user',
|
||||||
|
},
|
||||||
|
guest_id: {
|
||||||
|
id: 'guest_id',
|
||||||
|
username: 'guest_user',
|
||||||
|
roles: 'guest_user',
|
||||||
|
},
|
||||||
|
not_guest_id: {
|
||||||
|
id: 'not_guest_id',
|
||||||
|
username: 'not_guest_user',
|
||||||
|
roles: 'system_user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentUserId: 'user_id',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should render the guest tags on gms', () => {
|
||||||
|
const state = {
|
||||||
|
entities: {
|
||||||
|
channels,
|
||||||
|
users,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const gmMembers = [
|
||||||
|
TestHelper.getUserMock({
|
||||||
|
id: 'user_id',
|
||||||
|
username: 'regular_user',
|
||||||
|
roles: 'system_user',
|
||||||
|
}),
|
||||||
|
TestHelper.getUserMock({
|
||||||
|
id: 'guest_id',
|
||||||
|
username: 'guest_user',
|
||||||
|
roles: 'system_guest',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrapper = renderWithContext(
|
||||||
|
<ChannelHeaderTitleGroup gmMembers={gmMembers}/>,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
expect(wrapper.queryAllByText('GUEST').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render the guest tags on gms when no guest is in it', () => {
|
||||||
|
const state = {
|
||||||
|
entities: {
|
||||||
|
channels: channelsWithoutGuests,
|
||||||
|
users,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const gmMembers = [
|
||||||
|
TestHelper.getUserMock({
|
||||||
|
id: 'user_id',
|
||||||
|
username: 'regular_user',
|
||||||
|
roles: 'system_user',
|
||||||
|
}),
|
||||||
|
TestHelper.getUserMock({
|
||||||
|
id: 'not_guest_id',
|
||||||
|
username: 'not_guest_user',
|
||||||
|
roles: 'system_user',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrapper = renderWithContext(
|
||||||
|
<ChannelHeaderTitleGroup gmMembers={gmMembers}/>,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
expect(wrapper.queryAllByText('GUEST').length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {displayUsername, isGuest} from 'mattermost-redux/utils/user_utils';
|
||||||
|
|
||||||
|
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
gmMembers?: UserProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChannelHeaderTitleGroup = ({
|
||||||
|
gmMembers,
|
||||||
|
}: Props) => {
|
||||||
|
const currentUser = useSelector(getCurrentUser);
|
||||||
|
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
|
||||||
|
const channel = useSelector(getCurrentChannel);
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// map the displayname to the gm member users
|
||||||
|
const membersMap: Record<string, UserProfile[]> = {};
|
||||||
|
if (gmMembers) {
|
||||||
|
for (const user of gmMembers) {
|
||||||
|
if (user.id === currentUser.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const userDisplayName = displayUsername(user, teammateNameDisplaySetting);
|
||||||
|
|
||||||
|
if (!membersMap[userDisplayName]) {
|
||||||
|
membersMap[userDisplayName] = []; //Create an array for cases with same display name
|
||||||
|
}
|
||||||
|
|
||||||
|
membersMap[userDisplayName].push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayNames = channel.display_name.split(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{displayNames.map((displayName, index) => {
|
||||||
|
if (!membersMap[displayName]) {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = membersMap[displayName].shift();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={user?.id}>
|
||||||
|
{index > 0 && ', '}
|
||||||
|
{displayName}
|
||||||
|
{isGuest(user?.roles ?? '') && <GuestTag/>}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ChannelHeaderTitleGroup);
|
||||||
@@ -7,8 +7,6 @@ import {bindActionCreators} from 'redux';
|
|||||||
import type {ActionCreatorsMapObject, Dispatch} from 'redux';
|
import type {ActionCreatorsMapObject, Dispatch} from 'redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
favoriteChannel,
|
|
||||||
unfavoriteChannel,
|
|
||||||
updateChannelNotifyProps,
|
updateChannelNotifyProps,
|
||||||
} from 'mattermost-redux/actions/channels';
|
} from 'mattermost-redux/actions/channels';
|
||||||
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
|
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
|
||||||
@@ -16,12 +14,10 @@ import {General} from 'mattermost-redux/constants';
|
|||||||
import {
|
import {
|
||||||
getCurrentChannel,
|
getCurrentChannel,
|
||||||
getMyCurrentChannelMembership,
|
getMyCurrentChannelMembership,
|
||||||
isCurrentChannelFavorite,
|
|
||||||
isCurrentChannelMuted,
|
isCurrentChannelMuted,
|
||||||
getCurrentChannelStats,
|
getCurrentChannelStats,
|
||||||
} from 'mattermost-redux/selectors/entities/channels';
|
} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
import {getCurrentRelativeTeamUrl, getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentRelativeTeamUrl, getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {
|
import {
|
||||||
displayLastActiveLabel,
|
displayLastActiveLabel,
|
||||||
@@ -101,14 +97,12 @@ function makeMapStateToProps() {
|
|||||||
gmMembers,
|
gmMembers,
|
||||||
rhsState: getRhsState(state),
|
rhsState: getRhsState(state),
|
||||||
rhsOpen: getIsRhsOpen(state),
|
rhsOpen: getIsRhsOpen(state),
|
||||||
isFavorite: isCurrentChannelFavorite(state),
|
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
isMuted: isCurrentChannelMuted(state),
|
isMuted: isCurrentChannelMuted(state),
|
||||||
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
|
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
|
||||||
hasGuests: stats.guest_count > 0,
|
hasGuests: stats.guest_count > 0,
|
||||||
pinnedPostsCount: stats.pinnedpost_count,
|
pinnedPostsCount: stats.pinnedpost_count,
|
||||||
hasMoreThanOneTeam,
|
hasMoreThanOneTeam,
|
||||||
teammateNameDisplaySetting: getTeammateNameDisplaySetting(state),
|
|
||||||
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
||||||
announcementBarCount: getAnnouncementBarCount(state),
|
announcementBarCount: getAnnouncementBarCount(state),
|
||||||
customStatus,
|
customStatus,
|
||||||
@@ -125,8 +119,6 @@ function makeMapStateToProps() {
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Props['actions']>({
|
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Props['actions']>({
|
||||||
favoriteChannel,
|
|
||||||
unfavoriteChannel,
|
|
||||||
showPinnedPosts,
|
showPinnedPosts,
|
||||||
showChannelFiles,
|
showChannelFiles,
|
||||||
closeRightHandSide,
|
closeRightHandSide,
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user