Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,403 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ComponentProps} from 'react';
|
||||
|
||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import ChannelHeader from 'components/channel_header/channel_header';
|
||||
import ChannelInfoButton from 'components/channel_header/channel_info_button';
|
||||
import Markdown from 'components/markdown';
|
||||
import Constants, {RHSStates} from 'utils/constants';
|
||||
import {TestHelper} from '../../utils/test_helper';
|
||||
import {ChannelType} from '@mattermost/types/channels';
|
||||
import {UserCustomStatus} from '@mattermost/types/users';
|
||||
|
||||
describe('components/ChannelHeader', () => {
|
||||
const baseProps: ComponentProps<typeof ChannelHeader> = {
|
||||
actions: {
|
||||
favoriteChannel: jest.fn(),
|
||||
unfavoriteChannel: jest.fn(),
|
||||
showPinnedPosts: jest.fn(),
|
||||
showChannelFiles: jest.fn(),
|
||||
closeRightHandSide: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
getCustomEmojisInText: jest.fn(),
|
||||
updateChannelNotifyProps: jest.fn(),
|
||||
goToLastViewedChannel: jest.fn(),
|
||||
showChannelMembers: jest.fn(),
|
||||
},
|
||||
announcementBarCount: 1,
|
||||
teamId: 'team_id',
|
||||
channel: TestHelper.getChannelMock({}),
|
||||
channelMember: TestHelper.getChannelMembershipMock({}),
|
||||
currentUser: TestHelper.getUserMock({}),
|
||||
teammateNameDisplaySetting: '',
|
||||
currentRelativeTeamUrl: '',
|
||||
isCustomStatusEnabled: false,
|
||||
isCustomStatusExpired: false,
|
||||
isFileAttachmentsEnabled: true,
|
||||
lastActivityTimestamp: 1632146562846,
|
||||
isLastActiveEnabled: true,
|
||||
timestampUnits: [
|
||||
'now',
|
||||
'minute',
|
||||
'hour',
|
||||
],
|
||||
};
|
||||
|
||||
const populatedProps = {
|
||||
...baseProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
team_id: 'team_id',
|
||||
name: 'Test',
|
||||
delete_at: 0,
|
||||
}),
|
||||
channelMember: TestHelper.getChannelMembershipMock({
|
||||
channel_id: 'channel_id',
|
||||
user_id: 'user_id',
|
||||
}),
|
||||
currentUser: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
bot_description: 'the bot description',
|
||||
}),
|
||||
};
|
||||
|
||||
test('should render properly when empty', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render properly when populated', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...populatedProps}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render properly when populated with channel props', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
team_id: 'team_id',
|
||||
name: 'Test',
|
||||
header: 'See ~test',
|
||||
props: {
|
||||
channel_mentions: {
|
||||
test: {
|
||||
display_name: 'Test',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
channelMember: TestHelper.getChannelMembershipMock({
|
||||
channel_id: 'channel_id',
|
||||
user_id: 'user_id',
|
||||
}),
|
||||
currentUser: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
}),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render archived view', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
channel: {...populatedProps.channel, delete_at: 1234},
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render shared view', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
...populatedProps.channel,
|
||||
shared: true,
|
||||
type: Constants.OPEN_CHANNEL as ChannelType,
|
||||
}),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render correct menu when muted', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
isMuted: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should unmute the channel when mute icon is clicked', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
isMuted: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
|
||||
wrapper.find('.channel-header__mute').simulate('click');
|
||||
wrapper.update();
|
||||
expect(props.actions.updateChannelNotifyProps).toHaveBeenCalledTimes(1);
|
||||
expect(props.actions.updateChannelNotifyProps).toHaveBeenCalledWith('user_id', 'channel_id', {mark_unread: 'all'});
|
||||
});
|
||||
|
||||
test('should render active pinned posts', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
rhsState: RHSStates.PIN,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render active channel files', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
rhsState: RHSStates.CHANNEL_FILES,
|
||||
showChannelFilesButton: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render not active channel files', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
rhsState: RHSStates.PIN,
|
||||
showChannelFilesButton: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render active flagged posts', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
rhsState: RHSStates.FLAG,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render active mentions posts', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
rhsState: RHSStates.MENTION,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render bot description', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
header: 'not the bot description',
|
||||
type: Constants.DM_CHANNEL as ChannelType,
|
||||
}),
|
||||
dmUser: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
is_bot: true,
|
||||
bot_description: 'the bot description',
|
||||
}),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.containsMatchingElement(
|
||||
<Markdown
|
||||
message={props.currentUser.bot_description}
|
||||
/>,
|
||||
)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should render the pinned icon with the pinned posts count', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
pinnedPostsCount: 2,
|
||||
};
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
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', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
header: 'not the bot description',
|
||||
type: Constants.DM_CHANNEL as ChannelType,
|
||||
status: 'offline',
|
||||
}),
|
||||
dmUser: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
is_bot: false,
|
||||
}),
|
||||
isCustomStatusEnabled: true,
|
||||
customStatus: {
|
||||
emoji: 'calender',
|
||||
text: 'In a meeting',
|
||||
} as UserCustomStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render properly when custom status is expired', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
header: 'not the bot description',
|
||||
type: Constants.DM_CHANNEL as ChannelType,
|
||||
status: 'offline',
|
||||
}),
|
||||
dmUser: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
is_bot: false,
|
||||
}),
|
||||
isCustomStatusEnabled: true,
|
||||
isCustomStatusExpired: true,
|
||||
customStatus: {
|
||||
emoji: 'calender',
|
||||
text: 'In a meeting',
|
||||
} as UserCustomStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should contain the channel info button', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...populatedProps}/>,
|
||||
);
|
||||
expect(wrapper.contains(
|
||||
<ChannelInfoButton channel={populatedProps.channel}/>,
|
||||
)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should match snapshot with last active display', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
header: 'not the bot description',
|
||||
type: Constants.DM_CHANNEL as ChannelType,
|
||||
status: 'offline',
|
||||
}),
|
||||
dmUser: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
is_bot: false,
|
||||
props: {
|
||||
show_last_active: 'true',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with no last active display because it is disabled', () => {
|
||||
const props = {
|
||||
...populatedProps,
|
||||
isLastActiveEnabled: false,
|
||||
channel: TestHelper.getChannelMock({
|
||||
header: 'not the bot description',
|
||||
type: Constants.DM_CHANNEL as ChannelType,
|
||||
status: 'offline',
|
||||
}),
|
||||
dmUser: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
is_bot: false,
|
||||
props: {
|
||||
show_last_active: 'false',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
866
webapp/channels/src/components/channel_header/channel_header.tsx
Обычный файл
866
webapp/channels/src/components/channel_header/channel_header.tsx
Обычный файл
@@ -0,0 +1,866 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {MouseEvent, ReactNode, RefObject} from 'react';
|
||||
import {Overlay} from 'react-bootstrap';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
||||
import {displayUsername, isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||
import Markdown from 'components/markdown';
|
||||
import OverlayTrigger, {BaseOverlayTrigger} from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import ArchiveIcon from 'components/widgets/icons/archive_icon';
|
||||
import SharedChannelIndicator from 'components/shared_channel_indicator';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import {ChannelHeaderDropdown} from 'components/channel_header_dropdown';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
|
||||
import Popover from 'components/widgets/popover';
|
||||
import CallButton from 'plugins/call_button';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import ChannelHeaderPlug from 'plugins/channel_header_plug';
|
||||
|
||||
import {
|
||||
Constants,
|
||||
ModalIdentifiers,
|
||||
NotificationLevels,
|
||||
RHSStates,
|
||||
} from 'utils/constants';
|
||||
import {handleFormattedTextClick, localizeMessage, isEmptyObject, toTitleCase} from 'utils/utils';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
import {UserCustomStatus, UserProfile} from '@mattermost/types/users';
|
||||
import {Channel, ChannelMembership, ChannelNotifyProps} from '@mattermost/types/channels';
|
||||
import {RhsState} from 'types/store/rhs';
|
||||
|
||||
import {ModalData} from 'types/actions';
|
||||
|
||||
import LocalizedIcon from 'components/localized_icon';
|
||||
|
||||
import ChannelInfoButton from './channel_info_button';
|
||||
import HeaderIconWrapper from './components/header_icon_wrapper';
|
||||
|
||||
const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true};
|
||||
const popoverMarkdownOptions = {singleline: false, mentionHighlight: false, atMentions: true};
|
||||
|
||||
export type Props = {
|
||||
teamId: string;
|
||||
currentUser: UserProfile;
|
||||
channel: Channel;
|
||||
memberCount?: number;
|
||||
channelMember?: ChannelMembership;
|
||||
dmUser?: UserProfile;
|
||||
gmMembers?: UserProfile[];
|
||||
isFavorite?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isMuted?: boolean;
|
||||
hasGuests?: boolean;
|
||||
rhsState?: RhsState;
|
||||
rhsOpen?: boolean;
|
||||
isQuickSwitcherOpen?: boolean;
|
||||
intl: IntlShape;
|
||||
pinnedPostsCount?: number;
|
||||
hasMoreThanOneTeam?: boolean;
|
||||
actions: {
|
||||
favoriteChannel: (channelId: string) => void;
|
||||
unfavoriteChannel: (channelId: string) => void;
|
||||
showPinnedPosts: (channelId?: string) => void;
|
||||
showChannelFiles: (channelId: string) => void;
|
||||
closeRightHandSide: () => void;
|
||||
getCustomEmojisInText: (text: string) => void;
|
||||
updateChannelNotifyProps: (userId: string, channelId: string, props: Partial<ChannelNotifyProps>) => void;
|
||||
goToLastViewedChannel: () => void;
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
closeModal: () => void;
|
||||
showChannelMembers: (channelId: string, inEditingMode?: boolean) => void;
|
||||
};
|
||||
teammateNameDisplaySetting: string;
|
||||
currentRelativeTeamUrl: string;
|
||||
announcementBarCount: number;
|
||||
customStatus?: UserCustomStatus;
|
||||
isCustomStatusEnabled: boolean;
|
||||
isCustomStatusExpired: boolean;
|
||||
isFileAttachmentsEnabled: boolean;
|
||||
isLastActiveEnabled: boolean;
|
||||
timestampUnits?: string[];
|
||||
lastActivityTimestamp?: number;
|
||||
};
|
||||
|
||||
type State = {
|
||||
titleMenuOpen: boolean;
|
||||
showChannelHeaderPopover: boolean;
|
||||
leftOffset: number;
|
||||
topOffset: number;
|
||||
popoverOverlayWidth: number;
|
||||
};
|
||||
|
||||
class ChannelHeader extends React.PureComponent<Props, State> {
|
||||
toggleFavoriteRef: RefObject<HTMLButtonElement>;
|
||||
headerDescriptionRef: RefObject<HTMLSpanElement>;
|
||||
headerPopoverTextMeasurerRef: RefObject<HTMLDivElement>;
|
||||
headerOverlayRef: RefObject<BaseOverlayTrigger>;
|
||||
getHeaderMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
||||
getPopoverMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.toggleFavoriteRef = React.createRef();
|
||||
this.headerDescriptionRef = React.createRef();
|
||||
this.headerPopoverTextMeasurerRef = React.createRef();
|
||||
this.headerOverlayRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
popoverOverlayWidth: 0,
|
||||
showChannelHeaderPopover: false,
|
||||
leftOffset: 0,
|
||||
topOffset: 0,
|
||||
titleMenuOpen: false,
|
||||
};
|
||||
|
||||
this.getHeaderMarkdownOptions = memoizeResult((channelNamesMap: Record<string, any>) => (
|
||||
{...headerMarkdownOptions, channelNamesMap}
|
||||
));
|
||||
this.getPopoverMarkdownOptions = memoizeResult((channelNamesMap: Record<string, any>) => (
|
||||
{...popoverMarkdownOptions, channelNamesMap}
|
||||
));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.actions.getCustomEmojisInText(this.props.channel ? this.props.channel.header : '');
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const header = this.props.channel ? this.props.channel.header : '';
|
||||
const prevHeader = prevProps.channel ? prevProps.channel.header : '';
|
||||
if (header !== prevHeader) {
|
||||
this.props.actions.getCustomEmojisInText(header);
|
||||
}
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
const {actions, channel, channelMember, currentUser} = this.props;
|
||||
|
||||
if (!channelMember || !currentUser || !channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {mark_unread: NotificationLevels.ALL};
|
||||
actions.updateChannelNotifyProps(currentUser.id, channel.id, options);
|
||||
};
|
||||
|
||||
mute = () => {
|
||||
const {actions, channel, channelMember, currentUser} = this.props;
|
||||
|
||||
if (!channelMember || !currentUser || !channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {mark_unread: NotificationLevels.MENTION};
|
||||
actions.updateChannelNotifyProps(currentUser.id, channel.id, options);
|
||||
};
|
||||
|
||||
showPinnedPosts = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (this.props.rhsState === RHSStates.PIN) {
|
||||
this.props.actions.closeRightHandSide();
|
||||
} else {
|
||||
this.props.actions.showPinnedPosts();
|
||||
}
|
||||
};
|
||||
|
||||
showChannelFiles = () => {
|
||||
if (this.props.rhsState === RHSStates.CHANNEL_FILES) {
|
||||
this.props.actions.closeRightHandSide();
|
||||
} else {
|
||||
this.props.actions.showChannelFiles(this.props.channel.id);
|
||||
}
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
if (this.headerOverlayRef.current) {
|
||||
this.headerOverlayRef.current.hide();
|
||||
}
|
||||
|
||||
const {actions, channel} = this.props;
|
||||
const modalData = {
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||
dialogType: EditChannelHeaderModal,
|
||||
dialogProps: {channel},
|
||||
};
|
||||
|
||||
actions.openModal(modalData);
|
||||
}
|
||||
|
||||
showChannelHeaderPopover = (headerText: string) => {
|
||||
const headerDescriptionRect = this.headerDescriptionRef.current?.getBoundingClientRect();
|
||||
const headerPopoverTextMeasurerRect = this.headerPopoverTextMeasurerRef.current?.getBoundingClientRect();
|
||||
const announcementBarSize = 40;
|
||||
|
||||
if (headerPopoverTextMeasurerRect && headerDescriptionRect) {
|
||||
if (headerPopoverTextMeasurerRect.width > headerDescriptionRect.width || headerText.match(/\n{2,}/g)) {
|
||||
this.setState({showChannelHeaderPopover: true, leftOffset: this.headerDescriptionRef.current?.offsetLeft || 0});
|
||||
}
|
||||
}
|
||||
|
||||
// add 40px to take the global header into account
|
||||
const topOffset = (announcementBarSize * this.props.announcementBarCount) + 40;
|
||||
|
||||
this.setState({topOffset});
|
||||
}
|
||||
|
||||
toggleChannelMembersRHS = () => {
|
||||
if (this.props.rhsState === RHSStates.CHANNEL_MEMBERS) {
|
||||
this.props.actions.closeRightHandSide();
|
||||
} else {
|
||||
this.props.actions.showChannelMembers(this.props.channel.id);
|
||||
}
|
||||
};
|
||||
|
||||
setPopoverOverlayWidth = () => {
|
||||
const headerDescriptionRect = this.headerDescriptionRef.current?.getBoundingClientRect();
|
||||
const ellipsisWidthAdjustment = 10;
|
||||
this.setState({popoverOverlayWidth: (headerDescriptionRect?.width ?? 0) + ellipsisWidthAdjustment});
|
||||
}
|
||||
|
||||
handleFormattedTextClick = (e: MouseEvent<HTMLSpanElement>) => handleFormattedTextClick(e, this.props.currentRelativeTeamUrl);
|
||||
|
||||
renderCustomStatus = () => {
|
||||
const {customStatus, isCustomStatusEnabled, isCustomStatusExpired} = this.props;
|
||||
const isStatusSet = !isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
|
||||
if (!(isCustomStatusEnabled && isStatusSet)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='custom-emoji__wrapper'>
|
||||
<CustomStatusEmoji
|
||||
userID={this.props.dmUser?.id}
|
||||
showTooltip={true}
|
||||
tooltipDirection='bottom'
|
||||
emojiStyle={{
|
||||
verticalAlign: 'top',
|
||||
margin: '0 4px 1px',
|
||||
}}
|
||||
/>
|
||||
<CustomStatusText
|
||||
text={customStatus?.text}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
teamId,
|
||||
currentUser,
|
||||
gmMembers,
|
||||
channel,
|
||||
channelMember,
|
||||
isMuted: channelMuted,
|
||||
isReadOnly,
|
||||
isFavorite,
|
||||
dmUser,
|
||||
rhsState,
|
||||
hasGuests,
|
||||
teammateNameDisplaySetting,
|
||||
} = this.props;
|
||||
const {formatMessage} = this.props.intl;
|
||||
const ariaLabelChannelHeader = localizeMessage('accessibility.sections.channelHeader', 'channel header region');
|
||||
|
||||
let hasGuestsText: ReactNode = '';
|
||||
if (hasGuests) {
|
||||
hasGuestsText = (
|
||||
<span className='has-guest-header'>
|
||||
<span tabIndex={0}>
|
||||
<FormattedMessage
|
||||
id='channel_header.channelHasGuests'
|
||||
defaultMessage='This channel has guests'
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const channelIsArchived = channel.delete_at !== 0;
|
||||
if (isEmptyObject(channel) ||
|
||||
isEmptyObject(channelMember) ||
|
||||
isEmptyObject(currentUser) ||
|
||||
(!dmUser && channel.type === Constants.DM_CHANNEL)
|
||||
) {
|
||||
// Use an empty div to make sure the header's height stays constant
|
||||
return (
|
||||
<div className='channel-header'/>
|
||||
);
|
||||
}
|
||||
|
||||
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 isGroup = (channel.type === Constants.GM_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) {
|
||||
// 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) {
|
||||
hasGuestsText = (
|
||||
<span className='has-guest-header'>
|
||||
<FormattedMessage
|
||||
id='channel_header.groupMessageHasGuests'
|
||||
defaultMessage='This group message has guests'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let dmHeaderIconStatus: ReactNode;
|
||||
let dmHeaderTextStatus: ReactNode;
|
||||
if (isDirect && !dmUser?.delete_at && !dmUser?.is_bot) {
|
||||
dmHeaderIconStatus = (<StatusIcon status={channel.status}/>);
|
||||
|
||||
dmHeaderTextStatus = (
|
||||
<span className='header-status__text'>
|
||||
<FormattedMessage
|
||||
id={`status_dropdown.set_${channel.status}`}
|
||||
defaultMessage={toTitleCase(channel.status || '')}
|
||||
/>
|
||||
{this.renderCustomStatus()}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (this.props.isLastActiveEnabled && this.props.lastActivityTimestamp && this.props.timestampUnits) {
|
||||
dmHeaderTextStatus = (
|
||||
<span className='header-status__text'>
|
||||
<span className='last-active__text'>
|
||||
<FormattedMessage
|
||||
id='channel_header.lastActive'
|
||||
defaultMessage='Active {timestamp}'
|
||||
values={{
|
||||
timestamp: (
|
||||
<Timestamp
|
||||
value={this.props.lastActivityTimestamp}
|
||||
units={this.props.timestampUnits}
|
||||
useTime={false}
|
||||
style={'short'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{this.renderCustomStatus()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const channelFilesIconClass = classNames('channel-header__icon channel-header__icon--wide channel-header__icon--left', {
|
||||
'channel-header__icon--active': rhsState === RHSStates.CHANNEL_FILES,
|
||||
});
|
||||
const channelFilesIcon = <i className='icon icon-file-text-outline'/>;
|
||||
const pinnedIconClass = classNames('channel-header__icon channel-header__icon--wide channel-header__icon--left', {
|
||||
'channel-header__icon--active': rhsState === RHSStates.PIN,
|
||||
});
|
||||
const pinnedIcon = this.props.pinnedPostsCount ? (
|
||||
<>
|
||||
<i
|
||||
aria-hidden='true'
|
||||
className='icon icon-pin-outline channel-header__pin'
|
||||
/>
|
||||
<span
|
||||
id='channelPinnedPostCountText'
|
||||
className='icon__text'
|
||||
>
|
||||
{this.props.pinnedPostsCount}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<i
|
||||
aria-hidden='true'
|
||||
className='icon icon-pin-outline channel-header__pin'
|
||||
/>
|
||||
);
|
||||
|
||||
let memberListButton = null;
|
||||
if (!isDirect) {
|
||||
const membersIconClass = classNames('member-rhs__trigger channel-header__icon channel-header__icon--left channel-header__icon--wide', {
|
||||
'channel-header__icon--active': rhsState === RHSStates.CHANNEL_MEMBERS,
|
||||
});
|
||||
const membersIcon = this.props.memberCount ? (
|
||||
<>
|
||||
<i
|
||||
aria-hidden='true'
|
||||
className='icon icon-account-outline channel-header__members'
|
||||
/>
|
||||
<span
|
||||
id='channelMemberCountText'
|
||||
className='icon__text'
|
||||
>
|
||||
{this.props.memberCount}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i
|
||||
aria-hidden='true'
|
||||
className='icon icon-account-outline channel-header__members'
|
||||
/>
|
||||
<span
|
||||
id='channelMemberCountText'
|
||||
className='icon__text'
|
||||
>
|
||||
{'-'}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
memberListButton = (
|
||||
<HeaderIconWrapper
|
||||
iconComponent={membersIcon}
|
||||
ariaLabel={true}
|
||||
buttonClass={membersIconClass}
|
||||
buttonId={'member_rhs'}
|
||||
onClick={this.toggleChannelMembersRHS}
|
||||
tooltipKey={'channelMembers'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let headerTextContainer;
|
||||
const headerText = (isDirect && dmUser?.is_bot) ? dmUser.bot_description : channel.header;
|
||||
if (headerText) {
|
||||
const imageProps = {
|
||||
hideUtilities: true,
|
||||
};
|
||||
const popoverContent = (
|
||||
<Popover
|
||||
id='header-popover'
|
||||
popoverStyle='info'
|
||||
popoverSize='lg'
|
||||
style={{maxWidth: `${this.state.popoverOverlayWidth}px`, transform: `translate(${this.state.leftOffset}px, ${this.state.topOffset}px)`}}
|
||||
placement='bottom'
|
||||
className={classNames('channel-header__popover', {'chanel-header__popover--lhs_offset': this.props.hasMoreThanOneTeam})}
|
||||
>
|
||||
<span
|
||||
onClick={this.handleFormattedTextClick}
|
||||
>
|
||||
<Markdown
|
||||
message={headerText}
|
||||
options={this.getPopoverMarkdownOptions(channelNamesMap)}
|
||||
imageProps={imageProps}
|
||||
/>
|
||||
</span>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
headerTextContainer = (
|
||||
<div
|
||||
id='channelHeaderDescription'
|
||||
className='channel-header__description'
|
||||
dir='auto'
|
||||
>
|
||||
{dmHeaderIconStatus}
|
||||
{dmHeaderTextStatus}
|
||||
{memberListButton}
|
||||
|
||||
<HeaderIconWrapper
|
||||
iconComponent={pinnedIcon}
|
||||
ariaLabel={true}
|
||||
buttonClass={pinnedIconClass}
|
||||
buttonId={'channelHeaderPinButton'}
|
||||
onClick={this.showPinnedPosts}
|
||||
tooltipKey={'pinnedPosts'}
|
||||
/>
|
||||
{this.props.isFileAttachmentsEnabled &&
|
||||
<HeaderIconWrapper
|
||||
iconComponent={channelFilesIcon}
|
||||
ariaLabel={true}
|
||||
buttonClass={channelFilesIconClass}
|
||||
buttonId={'channelHeaderFilesButton'}
|
||||
onClick={this.showChannelFiles}
|
||||
tooltipKey={'channelFiles'}
|
||||
/>
|
||||
}
|
||||
{hasGuestsText}
|
||||
<div
|
||||
className='header-popover-text-measurer'
|
||||
ref={this.headerPopoverTextMeasurerRef}
|
||||
>
|
||||
<Markdown
|
||||
message={headerText.replace(/\n+/g, ' ')}
|
||||
options={this.getHeaderMarkdownOptions(channelNamesMap)}
|
||||
imageProps={imageProps}
|
||||
/></div>
|
||||
<span
|
||||
className='header-description__text'
|
||||
onClick={this.handleFormattedTextClick}
|
||||
onMouseOver={() => this.showChannelHeaderPopover(headerText)}
|
||||
onMouseOut={() => this.setState({showChannelHeaderPopover: false})}
|
||||
ref={this.headerDescriptionRef}
|
||||
>
|
||||
|
||||
<Overlay
|
||||
show={this.state.showChannelHeaderPopover}
|
||||
placement='bottom'
|
||||
rootClose={true}
|
||||
target={this.headerDescriptionRef.current as React.ReactInstance}
|
||||
ref={this.headerOverlayRef}
|
||||
onEnter={this.setPopoverOverlayWidth}
|
||||
onHide={() => this.setState({showChannelHeaderPopover: false})}
|
||||
>
|
||||
{popoverContent}
|
||||
</Overlay>
|
||||
|
||||
<Markdown
|
||||
message={headerText}
|
||||
options={this.getHeaderMarkdownOptions(channelNamesMap)}
|
||||
imageProps={imageProps}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let editMessage;
|
||||
if (!isReadOnly && !channelIsArchived) {
|
||||
if (isDirect || isGroup) {
|
||||
if (!isDirect || !dmUser?.is_bot) {
|
||||
editMessage = (
|
||||
<button
|
||||
className='header-placeholder style--none'
|
||||
onClick={this.showEditChannelHeaderModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.addChannelHeader'
|
||||
defaultMessage='Add a channel header'
|
||||
/>
|
||||
<LocalizedIcon
|
||||
className='icon icon-pencil-outline edit-icon'
|
||||
ariaLabel={{id: t('channel_header.editLink'), defaultMessage: 'Edit'}}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
editMessage = (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={teamId}
|
||||
permissions={[isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES]}
|
||||
>
|
||||
<button
|
||||
className='header-placeholder style--none'
|
||||
onClick={this.showEditChannelHeaderModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.addChannelHeader'
|
||||
defaultMessage='Add a channel header'
|
||||
/>
|
||||
<LocalizedIcon
|
||||
className='icon icon-pencil-outline edit-icon'
|
||||
ariaLabel={{id: t('channel_header.editLink'), defaultMessage: 'Edit'}}
|
||||
/>
|
||||
</button>
|
||||
</ChannelPermissionGate>
|
||||
);
|
||||
}
|
||||
}
|
||||
headerTextContainer = (
|
||||
<div
|
||||
id='channelHeaderDescription'
|
||||
className='channel-header__description light'
|
||||
>
|
||||
{dmHeaderIconStatus}
|
||||
{dmHeaderTextStatus}
|
||||
{memberListButton}
|
||||
|
||||
<HeaderIconWrapper
|
||||
iconComponent={pinnedIcon}
|
||||
ariaLabel={true}
|
||||
buttonClass={pinnedIconClass}
|
||||
buttonId={'channelHeaderPinButton'}
|
||||
onClick={this.showPinnedPosts}
|
||||
tooltipKey={'pinnedPosts'}
|
||||
/>
|
||||
{this.props.isFileAttachmentsEnabled &&
|
||||
<HeaderIconWrapper
|
||||
iconComponent={channelFilesIcon}
|
||||
ariaLabel={true}
|
||||
buttonClass={channelFilesIconClass}
|
||||
buttonId={'channelHeaderFilesButton'}
|
||||
onClick={this.showChannelFiles}
|
||||
tooltipKey={'channelFiles'}
|
||||
/>
|
||||
}
|
||||
{hasGuestsText}
|
||||
{editMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<Tooltip id='channelMutedTooltip'>
|
||||
<FormattedMessage
|
||||
id='channelHeader.unmute'
|
||||
defaultMessage='Unmute'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
let muteTrigger;
|
||||
if (channelMuted) {
|
||||
muteTrigger = (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='bottom'
|
||||
overlay={channelMutedTooltip}
|
||||
>
|
||||
<button
|
||||
id='toggleMute'
|
||||
onClick={this.unmute}
|
||||
className={'style--none color--link channel-header__mute inactive'}
|
||||
aria-label={formatMessage({id: 'generic_icons.muted', defaultMessage: 'Muted Icon'})}
|
||||
>
|
||||
<i className={'icon icon-bell-off-outline'}/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
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'
|
||||
aria-label={formatMessage({id: 'generic_icons.dropdown', defaultMessage: 'Dropdown Icon'}).toLowerCase()}
|
||||
/>
|
||||
</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 (
|
||||
<div
|
||||
id='channel-header'
|
||||
aria-label={ariaLabelChannelHeader}
|
||||
role='banner'
|
||||
tabIndex={-1}
|
||||
data-channelid={`${channel.id}`}
|
||||
className='channel-header alt a11y__region'
|
||||
data-a11y-sort-order='8'
|
||||
>
|
||||
<div className='flex-parent'>
|
||||
<div className='flex-child'>
|
||||
<div
|
||||
id='channelHeaderInfo'
|
||||
className='channel-header__info'
|
||||
>
|
||||
<div
|
||||
className='channel-header__title dropdown'
|
||||
>
|
||||
<div>
|
||||
{title}
|
||||
</div>
|
||||
{muteTrigger}
|
||||
</div>
|
||||
{headerTextContainer}
|
||||
</div>
|
||||
</div>
|
||||
<ChannelHeaderPlug
|
||||
channel={channel}
|
||||
channelMember={channelMember}
|
||||
/>
|
||||
<CallButton/>
|
||||
<ChannelInfoButton channel={channel}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ChannelHeader);
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
|
||||
import {closeRightHandSide, showChannelInfo} from 'actions/views/rhs';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
import {RhsState} from 'types/store/rhs';
|
||||
|
||||
import HeaderIconWrapper from './components/header_icon_wrapper';
|
||||
|
||||
interface Props {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const Icon = styled.i`
|
||||
font-size:18px;
|
||||
line-height:18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ChannelInfoButton = ({channel}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const rhsState: RhsState = useSelector(getRhsState);
|
||||
const isRhsOpen: boolean = useSelector(getIsRhsOpen);
|
||||
const isChannelInfo = rhsState === RHSStates.CHANNEL_INFO ||
|
||||
rhsState === RHSStates.CHANNEL_MEMBERS ||
|
||||
rhsState === RHSStates.CHANNEL_FILES ||
|
||||
rhsState === RHSStates.PIN;
|
||||
|
||||
const buttonActive = isRhsOpen && isChannelInfo;
|
||||
const toggleRHS = useCallback(() => {
|
||||
if (buttonActive) {
|
||||
const action = isChannelInfo ? closeRightHandSide() : showChannelInfo(channel.id);
|
||||
dispatch(action);
|
||||
} else {
|
||||
dispatch(showChannelInfo(channel.id));
|
||||
}
|
||||
}, [buttonActive, channel.id, isChannelInfo, dispatch]);
|
||||
|
||||
const tooltipKey = buttonActive ? 'closeChannelInfo' : 'openChannelInfo';
|
||||
|
||||
let buttonClass = 'channel-header__icon';
|
||||
if (buttonActive) {
|
||||
buttonClass += ' channel-header__icon--active-inverted';
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderIconWrapper
|
||||
buttonClass={buttonClass}
|
||||
buttonId='channel-info-btn'
|
||||
onClick={toggleRHS}
|
||||
ariaLabel={true}
|
||||
iconComponent={<Icon className='icon-information-outline'/>}
|
||||
tooltipKey={tooltipKey}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelInfoButton;
|
||||
@@ -0,0 +1,260 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on ChannelFilesIcon 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={400}
|
||||
overlay={
|
||||
<Tooltip
|
||||
className="channel-files"
|
||||
id="channelFilesTooltip"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Channel files"
|
||||
id="channel_header.channelFiles"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="channel_files_class"
|
||||
id="button_id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="icon icon-file-document-outline"
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on FlagIcon 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={400}
|
||||
overlay={
|
||||
<Tooltip
|
||||
className="text-nowrap"
|
||||
id="flaggedTooltip"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Saved posts"
|
||||
id="channel_header.flagged"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="button_class"
|
||||
id="button_id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FlagIcon
|
||||
aria-hidden="true"
|
||||
className="icon icon__flag"
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on MentionsIcon 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={400}
|
||||
overlay={
|
||||
<Tooltip
|
||||
className=""
|
||||
id="recentMentionsTooltip"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Recent mentions"
|
||||
id="channel_header.recentMentions"
|
||||
/>
|
||||
<Memo(KeyboardShortcutSequence)
|
||||
hideDescription={true}
|
||||
isInsideTooltip={true}
|
||||
shortcut={
|
||||
Object {
|
||||
"default": Object {
|
||||
"defaultMessage": "Recent mentions: Ctrl|Shift|M",
|
||||
"id": "shortcuts.nav.recent_mentions",
|
||||
},
|
||||
"mac": Object {
|
||||
"defaultMessage": "Recent mentions: ⌘|Shift|M",
|
||||
"id": "shortcuts.nav.recent_mentions.mac",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="button_class"
|
||||
id="button_id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<MentionsIcon
|
||||
aria-hidden="true"
|
||||
className="icon icon__mentions"
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on PinIcon 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={400}
|
||||
overlay={
|
||||
<Tooltip
|
||||
className="pinned-posts"
|
||||
id="pinnedPostTooltip"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Pinned posts"
|
||||
id="channel_header.pinnedPosts"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="pinned_posts_class"
|
||||
id="button_id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<PinIcon
|
||||
aria-hidden="true"
|
||||
className="icon icon__pin"
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on PluginIcon with tooltipText 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={400}
|
||||
overlay={
|
||||
<Tooltip
|
||||
className=""
|
||||
id="pluginTooltip"
|
||||
>
|
||||
<span>
|
||||
plugin_tooltip_text
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="button_class"
|
||||
id="button_id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on PluginIcon without tooltipText 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="flex-child"
|
||||
>
|
||||
<button
|
||||
className="button_class"
|
||||
id="button_id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on SearchIcon 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
delayShow={400}
|
||||
overlay={
|
||||
<Tooltip
|
||||
className=""
|
||||
id="searchTooltip"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Search"
|
||||
id="channel_header.search"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="search_class"
|
||||
id="button_id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<SearchIcon
|
||||
aria-hidden="true"
|
||||
className="icon icon__search"
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import FlagIcon from 'components/widgets/icons/flag_icon';
|
||||
import MentionsIcon from 'components/widgets/icons/mentions_icon';
|
||||
import PinIcon from 'components/widgets/icons/pin_icon';
|
||||
import SearchIcon from 'components/widgets/icons/search_icon';
|
||||
|
||||
import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper';
|
||||
|
||||
describe('components/channel_header/components/HeaderIconWrapper', () => {
|
||||
function emptyFunction() {} //eslint-disable-line no-empty-function
|
||||
const mentionsIcon = (
|
||||
<MentionsIcon
|
||||
className='icon icon__mentions'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
);
|
||||
|
||||
const baseProps = {
|
||||
iconComponent: mentionsIcon,
|
||||
buttonClass: 'button_class',
|
||||
buttonId: 'button_id',
|
||||
onClick: emptyFunction,
|
||||
tooltipKey: 'recentMentions',
|
||||
};
|
||||
|
||||
test('should match snapshot, on MentionsIcon', () => {
|
||||
const wrapper = shallow(
|
||||
<HeaderIconWrapper {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, on FlagIcon', () => {
|
||||
const flagIcon = (
|
||||
<FlagIcon
|
||||
className='icon icon__flag'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
);
|
||||
|
||||
const props = {...baseProps, iconComponent: flagIcon, tooltipKey: 'flaggedPosts'};
|
||||
const wrapper = shallow(
|
||||
<HeaderIconWrapper {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, on PinIcon', () => {
|
||||
const pinIcon = (
|
||||
<PinIcon
|
||||
className='icon icon__pin'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
);
|
||||
|
||||
const props = {...baseProps, iconComponent: pinIcon, tooltipKey: 'pinnedPosts', buttonClass: 'pinned_posts_class'};
|
||||
const wrapper = shallow(
|
||||
<HeaderIconWrapper {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, on ChannelFilesIcon', () => {
|
||||
const channelFilesIcon = <i className='icon icon-file-document-outline'/>;
|
||||
|
||||
const props = {...baseProps, iconComponent: channelFilesIcon, tooltipKey: 'channelFiles', buttonClass: 'channel_files_class'};
|
||||
const wrapper = shallow(
|
||||
<HeaderIconWrapper {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, on SearchIcon', () => {
|
||||
const searchIcon = (
|
||||
<SearchIcon
|
||||
className='icon icon__search'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
);
|
||||
|
||||
const props = {...baseProps, iconComponent: searchIcon, tooltipKey: 'search', buttonClass: 'search_class'};
|
||||
const wrapper = shallow(
|
||||
<HeaderIconWrapper {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, on PluginIcon with tooltipText', () => {
|
||||
const pluginIcon = (
|
||||
<i className='fa fa-anchor'/>
|
||||
);
|
||||
|
||||
const props = {...baseProps, iconComponent: pluginIcon, tooltipKey: 'plugin', tooltipText: 'plugin_tooltip_text'};
|
||||
const wrapper = shallow(
|
||||
<HeaderIconWrapper {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, on PluginIcon without tooltipText', () => {
|
||||
const pluginIcon = (
|
||||
<i className='fa fa-anchor'/>
|
||||
);
|
||||
|
||||
const props = {...baseProps, iconComponent: pluginIcon, tooltipKey: 'plugin'};
|
||||
const wrapper = shallow(
|
||||
<HeaderIconWrapper {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import NewChannelWithBoardTourTip from 'components/app_bar/new_channel_with_board_tour_tip';
|
||||
import KeyboardShortcutSequence, {
|
||||
KEYBOARD_SHORTCUTS,
|
||||
KeyboardShortcutDescriptor,
|
||||
} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
|
||||
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import {Constants, suitePluginIds} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
type Props = {
|
||||
ariaLabel?: boolean;
|
||||
buttonClass?: string;
|
||||
buttonId: string;
|
||||
iconComponent: React.ReactNode;
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
tooltipKey: string;
|
||||
tooltipText?: React.ReactNode;
|
||||
isRhsOpen?: boolean;
|
||||
pluginId?: string;
|
||||
}
|
||||
|
||||
type TooltipInfo = {
|
||||
class: string;
|
||||
id: string;
|
||||
messageID: string;
|
||||
message: string;
|
||||
keyboardShortcut?: KeyboardShortcutDescriptor;
|
||||
}
|
||||
|
||||
const HeaderIconWrapper = (props: Props) => {
|
||||
const {
|
||||
ariaLabel,
|
||||
buttonClass,
|
||||
buttonId,
|
||||
iconComponent,
|
||||
onClick,
|
||||
tooltipKey,
|
||||
tooltipText,
|
||||
isRhsOpen,
|
||||
pluginId,
|
||||
} = props;
|
||||
|
||||
const toolTips: Record<string, TooltipInfo> = {
|
||||
flaggedPosts: {
|
||||
class: 'text-nowrap',
|
||||
id: 'flaggedTooltip',
|
||||
messageID: t('channel_header.flagged'),
|
||||
message: 'Saved posts',
|
||||
},
|
||||
pinnedPosts: {
|
||||
class: 'pinned-posts',
|
||||
id: 'pinnedPostTooltip',
|
||||
messageID: t('channel_header.pinnedPosts'),
|
||||
message: 'Pinned posts',
|
||||
},
|
||||
recentMentions: {
|
||||
class: '',
|
||||
id: 'recentMentionsTooltip',
|
||||
messageID: t('channel_header.recentMentions'),
|
||||
message: 'Recent mentions',
|
||||
keyboardShortcut: KEYBOARD_SHORTCUTS.navMentions,
|
||||
},
|
||||
search: {
|
||||
class: '',
|
||||
id: 'searchTooltip',
|
||||
messageID: t('channel_header.search'),
|
||||
message: 'Search',
|
||||
},
|
||||
channelFiles: {
|
||||
class: 'channel-files',
|
||||
id: 'channelFilesTooltip',
|
||||
messageID: t('channel_header.channelFiles'),
|
||||
message: 'Channel files',
|
||||
},
|
||||
openChannelInfo: {
|
||||
class: 'channel-info',
|
||||
id: 'channelInfoTooltip',
|
||||
messageID: t('channel_header.openChannelInfo'),
|
||||
message: 'View Info',
|
||||
},
|
||||
closeChannelInfo: {
|
||||
class: 'channel-info',
|
||||
id: 'channelInfoTooltip',
|
||||
messageID: t('channel_header.closeChannelInfo'),
|
||||
message: 'Close info',
|
||||
},
|
||||
channelMembers: {
|
||||
class: 'channel-info',
|
||||
id: 'channelMembersTooltip',
|
||||
messageID: t('channel_header.channelMembers'),
|
||||
message: 'Members',
|
||||
},
|
||||
};
|
||||
|
||||
function getTooltip(key: string) {
|
||||
if (toolTips[key] == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
id={toolTips[key].id}
|
||||
className={toolTips[key].class}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={toolTips[key].messageID}
|
||||
defaultMessage={toolTips[key].message}
|
||||
/>
|
||||
{toolTips[key].keyboardShortcut &&
|
||||
<KeyboardShortcutSequence
|
||||
shortcut={toolTips[key].keyboardShortcut!}
|
||||
hideDescription={true}
|
||||
isInsideTooltip={true}
|
||||
/>
|
||||
}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
let tooltip;
|
||||
if (tooltipKey === 'plugin' && tooltipText) {
|
||||
tooltip = (
|
||||
<Tooltip
|
||||
id='pluginTooltip'
|
||||
className=''
|
||||
>
|
||||
<span>{tooltipText}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
tooltip = getTooltip(tooltipKey);
|
||||
}
|
||||
|
||||
let ariaLabelText;
|
||||
if (ariaLabel) {
|
||||
ariaLabelText = `${localizeMessage(toolTips[tooltipKey].messageID, toolTips[tooltipKey].message)}`;
|
||||
}
|
||||
|
||||
const boardsEnabled = pluginId === suitePluginIds.focalboard || pluginId === suitePluginIds.boards;
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
trigger={['hover', 'focus']}
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='bottom'
|
||||
overlay={isRhsOpen ? <></> : tooltip}
|
||||
>
|
||||
<button
|
||||
id={buttonId}
|
||||
aria-label={ariaLabelText}
|
||||
className={buttonClass || 'channel-header__icon'}
|
||||
onClick={onClick}
|
||||
>
|
||||
{iconComponent}
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
{boardsEnabled &&
|
||||
<NewChannelWithBoardTourTip
|
||||
pulsatingDotPlacement={'start'}
|
||||
pulsatingDotTranslate={{x: 0, y: -22}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex-child'>
|
||||
<button
|
||||
id={buttonId}
|
||||
className={buttonClass || 'channel-header__icon'}
|
||||
onClick={onClick}
|
||||
>
|
||||
{iconComponent}
|
||||
</button>
|
||||
</div>
|
||||
{boardsEnabled &&
|
||||
<NewChannelWithBoardTourTip
|
||||
pulsatingDotPlacement={'start'}
|
||||
pulsatingDotTranslate={{x: 0, y: -22}}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderIconWrapper;
|
||||
139
webapp/channels/src/components/channel_header/index.ts
Обычный файл
139
webapp/channels/src/components/channel_header/index.ts
Обычный файл
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
|
||||
import {
|
||||
favoriteChannel,
|
||||
unfavoriteChannel,
|
||||
updateChannelNotifyProps,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {
|
||||
getCurrentChannel,
|
||||
getMyCurrentChannelMembership,
|
||||
isCurrentChannelFavorite,
|
||||
isCurrentChannelMuted,
|
||||
getCurrentChannelStats,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
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 {
|
||||
displayLastActiveLabel,
|
||||
getCurrentUser,
|
||||
getLastActiveTimestampUnits,
|
||||
getLastActivityForUserId,
|
||||
getUser,
|
||||
makeGetProfilesInChannel,
|
||||
} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||
|
||||
import {goToLastViewedChannel} from 'actions/views/channel';
|
||||
import {openModal, closeModal} from 'actions/views/modals';
|
||||
import {
|
||||
showPinnedPosts,
|
||||
showChannelFiles,
|
||||
closeRightHandSide,
|
||||
showChannelMembers,
|
||||
} from 'actions/views/rhs';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {getAnnouncementBarCount} from 'selectors/views/announcement_bar';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {isFileAttachmentsEnabled} from 'utils/file_utils';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {Action} from 'mattermost-redux/types/actions';
|
||||
|
||||
import ChannelHeader, {Props} from './channel_header';
|
||||
|
||||
const EMPTY_CHANNEL = {};
|
||||
const EMPTY_CHANNEL_STATS = {member_count: 0, guest_count: 0, pinnedpost_count: 0, files_count: 0};
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const doGetProfilesInChannel = makeGetProfilesInChannel();
|
||||
const getCustomStatus = makeGetCustomStatus();
|
||||
let timestampUnits: string[] = [];
|
||||
|
||||
return function mapStateToProps(state: GlobalState) {
|
||||
const channel = getCurrentChannel(state) || EMPTY_CHANNEL;
|
||||
const user = getCurrentUser(state);
|
||||
const teams = getMyTeams(state);
|
||||
const hasMoreThanOneTeam = teams.length > 1;
|
||||
const config = getConfig(state);
|
||||
|
||||
let dmUser;
|
||||
let gmMembers;
|
||||
let customStatus;
|
||||
let lastActivityTimestamp;
|
||||
|
||||
if (channel && channel.type === General.DM_CHANNEL) {
|
||||
const dmUserId = getUserIdFromChannelName(user.id, channel.name);
|
||||
dmUser = getUser(state, dmUserId);
|
||||
customStatus = dmUser && getCustomStatus(state, dmUser.id);
|
||||
lastActivityTimestamp = dmUser && getLastActivityForUserId(state, dmUser.id);
|
||||
} else if (channel && channel.type === General.GM_CHANNEL) {
|
||||
gmMembers = doGetProfilesInChannel(state, channel.id);
|
||||
}
|
||||
const stats = getCurrentChannelStats(state) || EMPTY_CHANNEL_STATS;
|
||||
|
||||
let isLastActiveEnabled = false;
|
||||
if (dmUser) {
|
||||
isLastActiveEnabled = displayLastActiveLabel(state, dmUser.id);
|
||||
timestampUnits = getLastActiveTimestampUnits(state, dmUser.id);
|
||||
}
|
||||
|
||||
return {
|
||||
teamId: getCurrentTeamId(state),
|
||||
channel,
|
||||
channelMember: getMyCurrentChannelMembership(state),
|
||||
memberCount: stats.member_count,
|
||||
currentUser: user,
|
||||
dmUser,
|
||||
gmMembers,
|
||||
rhsState: getRhsState(state),
|
||||
rhsOpen: getIsRhsOpen(state),
|
||||
isFavorite: isCurrentChannelFavorite(state),
|
||||
isReadOnly: false,
|
||||
isMuted: isCurrentChannelMuted(state),
|
||||
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
|
||||
hasGuests: stats.guest_count > 0,
|
||||
pinnedPostsCount: stats.pinnedpost_count,
|
||||
hasMoreThanOneTeam,
|
||||
teammateNameDisplaySetting: getTeammateNameDisplaySetting(state),
|
||||
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
||||
announcementBarCount: getAnnouncementBarCount(state),
|
||||
customStatus,
|
||||
isCustomStatusEnabled: isCustomStatusEnabled(state),
|
||||
isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
|
||||
lastActivityTimestamp,
|
||||
isFileAttachmentsEnabled: isFileAttachmentsEnabled(config),
|
||||
isLastActiveEnabled,
|
||||
timestampUnits,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Props['actions']>({
|
||||
favoriteChannel,
|
||||
unfavoriteChannel,
|
||||
showPinnedPosts,
|
||||
showChannelFiles,
|
||||
closeRightHandSide,
|
||||
getCustomEmojisInText,
|
||||
updateChannelNotifyProps,
|
||||
goToLastViewedChannel,
|
||||
openModal,
|
||||
closeModal,
|
||||
showChannelMembers,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default withRouter<any, any>(connect(makeMapStateToProps, mapDispatchToProps)(ChannelHeader));
|
||||
Ссылка в новой задаче
Block a user