[MM-62384] Move ChannelHeaderPopover from BootstrapOverlay/Popper to Floating-UI (#29820)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
7cf040655f
Коммит
25a6501e10
@@ -150,12 +150,12 @@ describe('Direct Message', () => {
|
|||||||
cy.get('#edit_textbox').type(message).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
cy.get('#edit_textbox').type(message).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||||
|
|
||||||
// # Hover on channel header
|
// # Hover on channel header
|
||||||
cy.get('#channelHeaderDescription .header-description__text').trigger('mouseover');
|
cy.get('#channelHeaderDescription .header-description__text').trigger('mouseenter');
|
||||||
|
|
||||||
// * Verify changes have been applied on header
|
// * Verify changes have been applied on header
|
||||||
cy.wait(TIMEOUTS.HALF_SEC);
|
cy.wait(TIMEOUTS.HALF_SEC);
|
||||||
cy.get('#header-popover .popover-content').should('be.visible');
|
cy.get('.channel-header-text-popover').should('be.visible');
|
||||||
cy.get('#header-popover').find('.popover-content').should(($el) => {
|
cy.get('.channel-header-text-popover').should(($el) => {
|
||||||
expect($el.get(0).innerText).to.eq(expectedMessage);
|
expect($el.get(0).innerText).to.eq(expectedMessage);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,12 +58,13 @@ describe('Header', () => {
|
|||||||
|
|
||||||
updateAndVerifyChannelHeader('>', header);
|
updateAndVerifyChannelHeader('>', header);
|
||||||
|
|
||||||
// # Click the header to see the whole text
|
// # Hover on channel header
|
||||||
cy.get('#channelHeaderDescription').click();
|
cy.get('#channelHeaderDescription .header-description__text').trigger('mouseenter');
|
||||||
|
|
||||||
// * Check that no ellipsis is present
|
// * Check that no ellipsis is present
|
||||||
cy.get('#header-popover > div.popover-content').
|
cy.get('.channel-header-text-popover').should(($el) => {
|
||||||
should('have.html', `<span><blockquote>\n<p>${header}</p>\n</blockquote></span>`);
|
expect($el.get(0).innerText).to.eq(header);
|
||||||
|
});
|
||||||
|
|
||||||
cy.apiSaveMessageDisplayPreference('clean');
|
cy.apiSaveMessageDisplayPreference('clean');
|
||||||
});
|
});
|
||||||
@@ -75,12 +76,14 @@ function updateAndVerifyChannelHeader(prefix, header) {
|
|||||||
|
|
||||||
// * Should render blockquote if it starts with ">"
|
// * Should render blockquote if it starts with ">"
|
||||||
if (prefix === '>') {
|
if (prefix === '>') {
|
||||||
cy.get('#channelHeaderDescription > span > blockquote').should('be.visible');
|
cy.get('.header-description__text').within(() => {
|
||||||
|
cy.get('blockquote').should('be.visible');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// * Check if channel header description has ellipsis
|
// * Check if channel header description has ellipsis
|
||||||
cy.get('#channelHeaderDescription > .header-description__text').find('p').
|
cy.get('.header-description__text').
|
||||||
should('have.text', header).
|
should('include.text', header).
|
||||||
and('have.css', 'overflow', 'hidden').
|
and('have.css', 'overflow', 'hidden').
|
||||||
and('have.css', 'text-overflow', 'ellipsis');
|
and('have.css', 'text-overflow', 'ellipsis');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,28 +47,36 @@ describe('Multi-user group header', () => {
|
|||||||
cy.apiLogin(testUser);
|
cy.apiLogin(testUser);
|
||||||
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
|
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
|
||||||
|
|
||||||
// * no channel header is set
|
const header = 'peace and progress';
|
||||||
cy.get('button.header-placeholder').invoke('show').trigger('mouseover');
|
|
||||||
cy.contains('#channelHeaderDescription button span', 'Add a channel header').should('be.visible');
|
|
||||||
|
|
||||||
// # click add a channel heander
|
// * Verify that no channel header is set
|
||||||
cy.findByRoleExtended('button', {name: 'Add a channel header'}).should('be.visible').click();
|
cy.get('#channel-header').within(() => {
|
||||||
|
cy.findByText('Add a channel header').should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
// # type a header
|
// # Force click on button which is hidden and shows on hover
|
||||||
const header = 'this is a header!';
|
cy.findByText('Add a channel header').click({force: true});
|
||||||
|
|
||||||
|
// * Verify the modal open to add header
|
||||||
cy.get('#editChannelHeaderModalLabel').should('be.visible').wait(TIMEOUTS.ONE_SEC);
|
cy.get('#editChannelHeaderModalLabel').should('be.visible').wait(TIMEOUTS.ONE_SEC);
|
||||||
cy.get('textarea#edit_textbox').should('be.visible').type(`${header}{enter}`);
|
|
||||||
cy.get('#editChannelHeaderModalLabel').should('not.exist'); // wait for modal to disappear
|
// # Add the header in the modal
|
||||||
|
cy.findByPlaceholderText('Edit the Channel Header...').should('be.visible').type(`${header}{enter}`);
|
||||||
|
|
||||||
|
// # Wait for modal to disappear
|
||||||
|
cy.waitUntil(() => cy.get('#editChannelHeaderModalLabel').should('not.be.visible'));
|
||||||
|
|
||||||
// * text appears in the top center panel
|
// * text appears in the top center panel
|
||||||
cy.contains('#channelHeaderDescription span.header-description__text p', header);
|
cy.get('#channel-header').within(() => {
|
||||||
|
cy.findByText(header).should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
checkSystemMessage('updated the channel header');
|
checkSystemMessage('updated the channel header');
|
||||||
|
|
||||||
// * channel is marked as read for the current user
|
// * Channel is marked as read for the current user
|
||||||
cy.get(`#sidebarItem_${groupChannel.name}`).should(beRead);
|
cy.get(`#sidebarItem_${groupChannel.name}`).should(beRead);
|
||||||
|
|
||||||
// * channel is marked as unread for other user
|
// * Channel is marked as unread for other user
|
||||||
cy.apiLogout();
|
cy.apiLogout();
|
||||||
cy.apiLogin(userList[0]);
|
cy.apiLogin(userList[0]);
|
||||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||||
@@ -81,18 +89,22 @@ describe('Multi-user group header', () => {
|
|||||||
cy.apiLogin(testUser);
|
cy.apiLogin(testUser);
|
||||||
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
|
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
|
||||||
|
|
||||||
// * verify header is set
|
// * Verify that channel header is set
|
||||||
cy.contains('#channelHeaderDescription button span', 'Add a channel header').should('not.exist');
|
cy.get('#channel-header').within(() => {
|
||||||
|
cy.findByText('Add a channel header').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
const header = 'this is a new header!';
|
const header = 'In pursuit of peace and progress';
|
||||||
editHeader(header);
|
editHeader(header);
|
||||||
|
|
||||||
// * text appears at the top
|
// * Header text appears at the top
|
||||||
cy.contains('#channelHeaderDescription span.header-description__text p', header);
|
cy.get('#channel-header').within(() => {
|
||||||
|
cy.findByText(header).should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
checkSystemMessage('updated the channel header');
|
checkSystemMessage('updated the channel header');
|
||||||
|
|
||||||
// * channel is marked as unread for other users
|
// * Channel is marked as unread for other users
|
||||||
cy.apiLogout();
|
cy.apiLogout();
|
||||||
cy.apiLogin(userList[0]);
|
cy.apiLogin(userList[0]);
|
||||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||||
@@ -101,35 +113,43 @@ describe('Multi-user group header', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T473_2 Edit GM channel header', () => {
|
it('MM-T473_2 Edit GM channel header', () => {
|
||||||
// # open existing GM
|
// # Open existing GM
|
||||||
cy.apiLogin(testUser);
|
cy.apiLogin(testUser);
|
||||||
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
|
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
|
||||||
|
|
||||||
// * verify header is set
|
// * Verify that channel header is set
|
||||||
cy.contains('#channelHeaderDescription button span', 'Add a channel header').should('not.exist');
|
cy.get('#channel-header').within(() => {
|
||||||
|
cy.findByText('Add a channel header').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
const header = `Header by @${testUser.username}`;
|
const header = `In pursuit of peace and progress by @${testUser.username}`;
|
||||||
editHeader(header);
|
editHeader(header);
|
||||||
|
|
||||||
cy.get('#channelHeaderDescription').find('.header-description__text').
|
// * Header text appears at the top
|
||||||
find('.mention-link').
|
cy.get('#channel-header').within(() => {
|
||||||
should('be.visible').and('have.text', `@${testUser.username}`);
|
// * Verify mention is present
|
||||||
cy.get('#channelHeaderDescription').find('.header-description__text').
|
cy.get('.mention-link').should('be.visible').and('have.text', `@${testUser.username}`);
|
||||||
find('.mention--highlight').
|
|
||||||
should('not.exist');
|
// * Verify its not highlighted
|
||||||
|
cy.get('.mention--highlight').should('not.exist');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const editHeader = (header) => {
|
function editHeader(header) {
|
||||||
// # Click edit conversation header
|
// # Click edit conversation header
|
||||||
cy.uiOpenChannelMenu('Edit Conversation Header');
|
cy.uiOpenChannelMenu('Edit Conversation Header');
|
||||||
|
|
||||||
// # type new header
|
// * Verify the modal open to add header
|
||||||
cy.get('#editChannelHeaderModalLabel').should('be.visible');
|
cy.get('#editChannelHeaderModalLabel').should('be.visible').wait(TIMEOUTS.ONE_SEC);
|
||||||
cy.get('textarea#edit_textbox').should('be.visible').clear().type(`${header}{enter}`);
|
|
||||||
cy.get('#editChannelHeaderModalLabel').should('not.exist'); // wait for modal to disappear
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkSystemMessage = (message) => {
|
// # Add the header in the modal
|
||||||
|
cy.get('textarea#edit_textbox').should('be.visible').clear().type(`${header}{enter}`);
|
||||||
|
|
||||||
|
// # Wait for modal to disappear
|
||||||
|
cy.waitUntil(() => cy.get('#editChannelHeaderModalLabel').should('not.be.visible'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSystemMessage(message) {
|
||||||
// * system message is posted notifying of the change
|
// * system message is posted notifying of the change
|
||||||
cy.getLastPostId().then((id) => {
|
cy.getLastPostId().then((id) => {
|
||||||
cy.get(`#postMessageText_${id}`).should('contain', message);
|
cy.get(`#postMessageText_${id}`).should('contain', message);
|
||||||
@@ -138,5 +158,5 @@ describe('Multi-user group header', () => {
|
|||||||
cy.get(`#delete_post_${id}`);
|
cy.get(`#delete_post_${id}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -2,48 +2,56 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type {ComponentProps} from 'react';
|
|
||||||
|
|
||||||
import type {ChannelType} from '@mattermost/types/channels';
|
import type {ChannelType} from '@mattermost/types/channels';
|
||||||
import type {UserCustomStatus} from '@mattermost/types/users';
|
import type {UserCustomStatus} from '@mattermost/types/users';
|
||||||
|
|
||||||
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 type {MockIntl} from 'tests/helpers/intl-test-helper';
|
||||||
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';
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
|
import ChannelHeader from './channel_header';
|
||||||
|
import type {Props} from './channel_header';
|
||||||
|
|
||||||
describe('components/ChannelHeader', () => {
|
describe('components/ChannelHeader', () => {
|
||||||
const baseProps: ComponentProps<typeof ChannelHeader> = {
|
const baseProps: Props = {
|
||||||
actions: {
|
actions: {
|
||||||
showPinnedPosts: jest.fn(),
|
showPinnedPosts: jest.fn(),
|
||||||
showChannelFiles: jest.fn(),
|
showChannelFiles: jest.fn(),
|
||||||
closeRightHandSide: jest.fn(),
|
closeRightHandSide: jest.fn(),
|
||||||
openModal: jest.fn(),
|
|
||||||
getCustomEmojisInText: jest.fn(),
|
getCustomEmojisInText: jest.fn(),
|
||||||
updateChannelNotifyProps: jest.fn(),
|
updateChannelNotifyProps: jest.fn(),
|
||||||
goToLastViewedChannel: jest.fn(),
|
|
||||||
showChannelMembers: jest.fn(),
|
showChannelMembers: jest.fn(),
|
||||||
},
|
},
|
||||||
announcementBarCount: 1,
|
|
||||||
teamId: 'team_id',
|
teamId: 'team_id',
|
||||||
channel: TestHelper.getChannelMock({}),
|
channel: TestHelper.getChannelMock({}),
|
||||||
channelMember: TestHelper.getChannelMembershipMock({}),
|
channelMember: TestHelper.getChannelMembershipMock({}),
|
||||||
currentUser: TestHelper.getUserMock({}),
|
currentUser: TestHelper.getUserMock({}),
|
||||||
currentRelativeTeamUrl: '',
|
|
||||||
isCustomStatusEnabled: false,
|
isCustomStatusEnabled: false,
|
||||||
isCustomStatusExpired: false,
|
isCustomStatusExpired: false,
|
||||||
isFileAttachmentsEnabled: true,
|
isFileAttachmentsEnabled: true,
|
||||||
lastActivityTimestamp: 1632146562846,
|
lastActivityTimestamp: 1632146562846,
|
||||||
isLastActiveEnabled: true,
|
isLastActiveEnabled: true,
|
||||||
|
memberCount: 2,
|
||||||
|
dmUser: undefined,
|
||||||
|
gmMembers: undefined,
|
||||||
|
rhsState: RHSStates.CHANNEL_INFO,
|
||||||
|
isChannelMuted: false,
|
||||||
|
hasGuests: false,
|
||||||
|
pinnedPostsCount: 0,
|
||||||
|
customStatus: undefined,
|
||||||
timestampUnits: [
|
timestampUnits: [
|
||||||
'now',
|
'now',
|
||||||
'minute',
|
'minute',
|
||||||
'hour',
|
'hour',
|
||||||
],
|
],
|
||||||
hideGuestTags: false,
|
hideGuestTags: false,
|
||||||
|
intl: {
|
||||||
|
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
|
||||||
|
} as MockIntl,
|
||||||
};
|
};
|
||||||
|
|
||||||
const populatedProps = {
|
const populatedProps = {
|
||||||
@@ -140,7 +148,7 @@ describe('components/ChannelHeader', () => {
|
|||||||
test('should render correct menu when muted', () => {
|
test('should render correct menu when muted', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...populatedProps,
|
...populatedProps,
|
||||||
isMuted: true,
|
isChannelMuted: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallowWithIntl(
|
const wrapper = shallowWithIntl(
|
||||||
@@ -152,7 +160,7 @@ describe('components/ChannelHeader', () => {
|
|||||||
test('should unmute the channel when mute icon is clicked', () => {
|
test('should unmute the channel when mute icon is clicked', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...populatedProps,
|
...populatedProps,
|
||||||
isMuted: true,
|
isChannelMuted: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallowWithIntl(
|
const wrapper = shallowWithIntl(
|
||||||
@@ -227,30 +235,6 @@ describe('components/ChannelHeader', () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
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', () => {
|
test('should render the pinned icon with the pinned posts count', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...populatedProps,
|
...populatedProps,
|
||||||
|
|||||||
@@ -4,125 +4,38 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type {MouseEvent, ReactNode, RefObject} from 'react';
|
import type {MouseEvent, ReactNode, RefObject} from 'react';
|
||||||
// eslint-disable-next-line no-restricted-imports
|
|
||||||
import type {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap';
|
|
||||||
import {Overlay} from 'react-bootstrap';
|
|
||||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {WrappedComponentProps} from 'react-intl';
|
||||||
|
|
||||||
import type {Channel, ChannelMembership, ChannelNotifyProps} from '@mattermost/types/channels';
|
|
||||||
import type {UserCustomStatus, UserProfile} from '@mattermost/types/users';
|
|
||||||
|
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
|
||||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
|
||||||
|
|
||||||
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 Markdown from 'components/markdown';
|
|
||||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
|
||||||
import Timestamp from 'components/timestamp';
|
import Timestamp from 'components/timestamp';
|
||||||
import Popover from 'components/widgets/popover';
|
|
||||||
import WithTooltip from 'components/with_tooltip';
|
import WithTooltip from 'components/with_tooltip';
|
||||||
|
|
||||||
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';
|
||||||
import {
|
import {
|
||||||
Constants,
|
Constants,
|
||||||
ModalIdentifiers,
|
|
||||||
NotificationLevels,
|
NotificationLevels,
|
||||||
RHSStates,
|
RHSStates,
|
||||||
} from 'utils/constants';
|
} from 'utils/constants';
|
||||||
import {handleFormattedTextClick, isEmptyObject} from 'utils/utils';
|
import {isEmptyObject} from 'utils/utils';
|
||||||
|
|
||||||
import type {ModalData} from 'types/actions';
|
|
||||||
import type {RhsState} from 'types/store/rhs';
|
|
||||||
|
|
||||||
|
import ChannelHeaderText from './channel_header_text';
|
||||||
import ChannelHeaderTitle from './channel_header_title';
|
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';
|
||||||
|
|
||||||
const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true};
|
import type {PropsFromRedux} from './index';
|
||||||
const popoverMarkdownOptions = {singleline: false, mentionHighlight: false, atMentions: true};
|
|
||||||
|
|
||||||
export type OverlayTrigger = BaseOverlayTrigger & {
|
export type Props = WrappedComponentProps & PropsFromRedux;
|
||||||
hide: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Props = {
|
class ChannelHeader extends React.PureComponent<Props> {
|
||||||
teamId: string;
|
|
||||||
currentUser: UserProfile;
|
|
||||||
channel?: Channel;
|
|
||||||
memberCount?: number;
|
|
||||||
channelMember?: ChannelMembership;
|
|
||||||
dmUser?: UserProfile;
|
|
||||||
gmMembers?: UserProfile[];
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
isMuted?: boolean;
|
|
||||||
hasGuests?: boolean;
|
|
||||||
rhsState?: RhsState;
|
|
||||||
rhsOpen?: boolean;
|
|
||||||
isQuickSwitcherOpen?: boolean;
|
|
||||||
intl: IntlShape;
|
|
||||||
pinnedPostsCount?: number;
|
|
||||||
hasMoreThanOneTeam?: boolean;
|
|
||||||
actions: {
|
|
||||||
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;
|
|
||||||
showChannelMembers: (channelId: string, inEditingMode?: boolean) => void;
|
|
||||||
};
|
|
||||||
currentRelativeTeamUrl: string;
|
|
||||||
announcementBarCount: number;
|
|
||||||
customStatus?: UserCustomStatus;
|
|
||||||
isCustomStatusEnabled: boolean;
|
|
||||||
isCustomStatusExpired: boolean;
|
|
||||||
isFileAttachmentsEnabled: boolean;
|
|
||||||
isLastActiveEnabled: boolean;
|
|
||||||
timestampUnits?: string[];
|
|
||||||
lastActivityTimestamp?: number;
|
|
||||||
hideGuestTags: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
showChannelHeaderPopover: boolean;
|
|
||||||
channelHeaderPoverWidth: number;
|
|
||||||
leftOffset: number;
|
|
||||||
topOffset: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChannelHeader extends React.PureComponent<Props, State> {
|
|
||||||
toggleFavoriteRef: RefObject<HTMLButtonElement>;
|
toggleFavoriteRef: RefObject<HTMLButtonElement>;
|
||||||
headerDescriptionRef: RefObject<HTMLSpanElement>;
|
|
||||||
headerPopoverTextMeasurerRef: RefObject<HTMLDivElement>;
|
|
||||||
headerOverlayRef: RefObject<OverlayTrigger>;
|
|
||||||
getHeaderMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
|
||||||
getPopoverMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.toggleFavoriteRef = React.createRef();
|
this.toggleFavoriteRef = React.createRef();
|
||||||
this.headerDescriptionRef = React.createRef();
|
|
||||||
this.headerPopoverTextMeasurerRef = React.createRef();
|
|
||||||
this.headerOverlayRef = React.createRef();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
showChannelHeaderPopover: false,
|
|
||||||
channelHeaderPoverWidth: 0,
|
|
||||||
leftOffset: 0,
|
|
||||||
topOffset: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getHeaderMarkdownOptions = memoizeResult((channelNamesMap: Record<string, any>) => (
|
|
||||||
{...headerMarkdownOptions, channelNamesMap}
|
|
||||||
));
|
|
||||||
this.getPopoverMarkdownOptions = memoizeResult((channelNamesMap: Record<string, any>) => (
|
|
||||||
{...popoverMarkdownOptions, channelNamesMap}
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -137,8 +50,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClose = () => this.props.actions.goToLastViewedChannel();
|
|
||||||
|
|
||||||
unmute = () => {
|
unmute = () => {
|
||||||
const {actions, channel, channelMember, currentUser} = this.props;
|
const {actions, channel, channelMember, currentUser} = this.props;
|
||||||
|
|
||||||
@@ -150,17 +61,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
actions.updateChannelNotifyProps(currentUser.id, channel.id, options);
|
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>) => {
|
showPinnedPosts = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.props.rhsState === RHSStates.PIN) {
|
if (this.props.rhsState === RHSStates.PIN) {
|
||||||
@@ -178,45 +78,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
showEditChannelHeaderModal = () => {
|
|
||||||
if (this.headerOverlayRef.current) {
|
|
||||||
this.headerOverlayRef.current.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
const {actions, channel} = this.props;
|
|
||||||
if (!channel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
const leftOffset = headerDescriptionRect.left - (this.props.hasMoreThanOneTeam ? 313 : 248);
|
|
||||||
this.setState({showChannelHeaderPopover: true, leftOffset});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add 40px to take the global header into account
|
|
||||||
const topOffset = (announcementBarSize * this.props.announcementBarCount) + 40;
|
|
||||||
const channelHeaderPoverWidth = this.headerDescriptionRef.current?.clientWidth || 0 - (this.props.hasMoreThanOneTeam ? 64 : 0);
|
|
||||||
|
|
||||||
this.setState({topOffset});
|
|
||||||
this.setState({channelHeaderPoverWidth});
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleChannelMembersRHS = () => {
|
toggleChannelMembersRHS = () => {
|
||||||
if (this.props.rhsState === RHSStates.CHANNEL_MEMBERS) {
|
if (this.props.rhsState === RHSStates.CHANNEL_MEMBERS) {
|
||||||
this.props.actions.closeRightHandSide();
|
this.props.actions.closeRightHandSide();
|
||||||
@@ -225,8 +86,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFormattedTextClick = (e: MouseEvent<HTMLSpanElement>) => handleFormattedTextClick(e, this.props.currentRelativeTeamUrl);
|
|
||||||
|
|
||||||
renderCustomStatus = () => {
|
renderCustomStatus = () => {
|
||||||
const {customStatus, isCustomStatusEnabled, isCustomStatusExpired} = this.props;
|
const {customStatus, isCustomStatusEnabled, isCustomStatusExpired} = this.props;
|
||||||
const isStatusSet = !isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
|
const isStatusSet = !isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
|
||||||
@@ -259,8 +118,7 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
gmMembers,
|
gmMembers,
|
||||||
channel,
|
channel,
|
||||||
channelMember,
|
channelMember,
|
||||||
isMuted: channelMuted,
|
isChannelMuted,
|
||||||
isReadOnly,
|
|
||||||
dmUser,
|
dmUser,
|
||||||
rhsState,
|
rhsState,
|
||||||
hasGuests,
|
hasGuests,
|
||||||
@@ -270,7 +128,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {formatMessage} = this.props.intl;
|
|
||||||
const ariaLabelChannelHeader = this.props.intl.formatMessage({id: 'accessibility.sections.channelHeader', defaultMessage: 'channel header region'});
|
const ariaLabelChannelHeader = this.props.intl.formatMessage({id: 'accessibility.sections.channelHeader', defaultMessage: 'channel header region'});
|
||||||
|
|
||||||
let hasGuestsText: ReactNode = '';
|
let hasGuestsText: ReactNode = '';
|
||||||
@@ -287,7 +144,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelIsArchived = channel.delete_at !== 0;
|
|
||||||
if (isEmptyObject(channel) ||
|
if (isEmptyObject(channel) ||
|
||||||
isEmptyObject(channelMember) ||
|
isEmptyObject(channelMember) ||
|
||||||
isEmptyObject(currentUser) ||
|
isEmptyObject(currentUser) ||
|
||||||
@@ -299,11 +155,8 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelNamesMap = channel.props && channel.props.channel_mentions;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
if (hasGuests && !hideGuestTags) {
|
if (hasGuests && !hideGuestTags) {
|
||||||
@@ -436,136 +289,8 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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={{transform: `translate(${this.state.leftOffset}px, ${this.state.topOffset}px)`, maxWidth: this.state.channelHeaderPoverWidth + 16}}
|
|
||||||
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'
|
|
||||||
>
|
|
||||||
{dmHeaderTextStatus}
|
|
||||||
{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 as any}
|
|
||||||
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'
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
className='icon icon-pencil-outline edit-icon'
|
|
||||||
aria-label={this.props.intl.formatMessage({id: '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'
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
className='icon icon-pencil-outline edit-icon'
|
|
||||||
aria-label={this.props.intl.formatMessage({id: 'channel_header.editLink', defaultMessage: 'Edit'})}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</ChannelPermissionGate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
headerTextContainer = (
|
|
||||||
<div
|
|
||||||
id='channelHeaderDescription'
|
|
||||||
className='channel-header__description'
|
|
||||||
>
|
|
||||||
{dmHeaderTextStatus}
|
|
||||||
{hasGuestsText}
|
|
||||||
{editMessage}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let muteTrigger;
|
let muteTrigger;
|
||||||
if (channelMuted) {
|
if (isChannelMuted) {
|
||||||
muteTrigger = (
|
muteTrigger = (
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
title={
|
title={
|
||||||
@@ -579,9 +304,12 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
id='toggleMute'
|
id='toggleMute'
|
||||||
onClick={this.unmute}
|
onClick={this.unmute}
|
||||||
className={'channel-header__mute inactive btn btn-icon btn-xs'}
|
className={'channel-header__mute inactive btn btn-icon btn-xs'}
|
||||||
aria-label={formatMessage({id: 'generic_icons.muted', defaultMessage: 'Muted Icon'})}
|
aria-label={this.props.intl.formatMessage({id: 'channelHeader.unmute', defaultMessage: 'Unmute'})}
|
||||||
>
|
>
|
||||||
<i className={'icon icon-bell-off-outline'}/>
|
<i
|
||||||
|
className={'icon icon-bell-off-outline'}
|
||||||
|
aria-hidden={true}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</WithTooltip>
|
</WithTooltip>
|
||||||
);
|
);
|
||||||
@@ -627,7 +355,18 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
</HeaderIconWrapper>
|
</HeaderIconWrapper>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{headerTextContainer}
|
<div
|
||||||
|
id='channelHeaderDescription'
|
||||||
|
className='channel-header__description'
|
||||||
|
>
|
||||||
|
{dmHeaderTextStatus}
|
||||||
|
{hasGuestsText}
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={teamId}
|
||||||
|
channel={channel}
|
||||||
|
dmUser={dmUser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||||
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
|
import ChannelHeaderText from './channel_header_text';
|
||||||
|
|
||||||
|
describe('ChannelHeaderText', () => {
|
||||||
|
const defaultTeamId = TestHelper.getTeamMock().id;
|
||||||
|
|
||||||
|
test('should render channel header text when header exists for a channel', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({header: 'Test Header'});
|
||||||
|
renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Header')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render channel header of bot description for bot DM channels', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({type: 'D'});
|
||||||
|
const botDm = TestHelper.getUserMock({is_bot: true, bot_description: 'Tranquility'});
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
dmUser={botDm}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Tranquility')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the channel has no header and is archived', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({delete_at: 1, header: ''});
|
||||||
|
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.childNodes.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if its a bot DM channels and its description is empty', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({type: 'D'});
|
||||||
|
const botDm = TestHelper.getUserMock({is_bot: true, bot_description: ''});
|
||||||
|
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
dmUser={botDm}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.childNodes.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show add header button for DM channels without header', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({type: 'D', header: ''});
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Add a channel header')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show add header button for GM channels', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({type: 'G', header: ''});
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Add a channel header')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not show add header button when user lacks permission and channel doesn not have header', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({
|
||||||
|
type: 'O',
|
||||||
|
header: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
entities: {
|
||||||
|
channels: {
|
||||||
|
channels: {
|
||||||
|
[channel.id]: channel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
roles: {
|
||||||
|
channel_user: {
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
/>,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Add a channel header')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show add header button when user has permission and channel does not have header', () => {
|
||||||
|
const channel = TestHelper.getChannelMock({
|
||||||
|
type: 'O',
|
||||||
|
header: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
entities: {
|
||||||
|
channels: {
|
||||||
|
myMembers: {
|
||||||
|
[channel.id]: {channel_id: channel.id, roles: 'channel_role'},
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
[channel.id]: new Set(['channel_role']),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
myMembers: {
|
||||||
|
[defaultTeamId]: {team_id: defaultTeamId, roles: 'team_role'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
currentUserId: 'user_id',
|
||||||
|
profiles: {
|
||||||
|
user_id: {
|
||||||
|
id: 'user_id',
|
||||||
|
roles: 'system_role',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
roles: {
|
||||||
|
system_role: {permissions: ['test_system_permission']},
|
||||||
|
team_role: {permissions: ['test_team_permission']},
|
||||||
|
channel_role: {permissions: ['manage_public_channel_properties']},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<ChannelHeaderText
|
||||||
|
teamId={defaultTeamId}
|
||||||
|
channel={channel}
|
||||||
|
/>,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Add a channel header')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// 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 {useDispatch} from 'react-redux';
|
||||||
|
|
||||||
|
import type {Channel} from '@mattermost/types/channels';
|
||||||
|
import type {Team} from '@mattermost/types/teams';
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
|
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||||
|
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Constants,
|
||||||
|
ModalIdentifiers,
|
||||||
|
} from 'utils/constants';
|
||||||
|
import {isChannelNamesMap} from 'utils/text_formatting';
|
||||||
|
|
||||||
|
import {ChannelHeaderTextPopover} from './channel_header_text_popover';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
teamId: Team['id'];
|
||||||
|
channel: Channel;
|
||||||
|
dmUser?: UserProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChannelHeaderText(props: Props) {
|
||||||
|
const isArchivedChannel = props.channel.delete_at !== 0;
|
||||||
|
const isDirectChannel = props.channel.type === Constants.DM_CHANNEL;
|
||||||
|
const isGroupChannel = props.channel.type === Constants.GM_CHANNEL;
|
||||||
|
const isBotDMChannel = isDirectChannel && (props.dmUser?.is_bot ?? false);
|
||||||
|
const isPrivateChannel = props.channel.type === Constants.PRIVATE_CHANNEL;
|
||||||
|
const headerText = isBotDMChannel ? props.dmUser?.bot_description ?? '' : props.channel?.header ?? '';
|
||||||
|
const hasHeaderText = headerText.trim().length > 0;
|
||||||
|
|
||||||
|
// If it has a channel then show the channel irrespective of the channel type/state etc
|
||||||
|
if (hasHeaderText) {
|
||||||
|
return (
|
||||||
|
<ChannelHeaderTextPopover
|
||||||
|
text={headerText}
|
||||||
|
channelMentionsNameMap={
|
||||||
|
isChannelNamesMap(props.channel?.props?.channel_mentions) ? props.channel.props.channel_mentions : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If doesn't have a header text then we need to check based on below
|
||||||
|
// conditions if we need to show button to add or not
|
||||||
|
|
||||||
|
if (isArchivedChannel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBotDMChannel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirectChannel || isGroupChannel) {
|
||||||
|
return <AddChannelHeaderTextButton channel={props.channel}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should show option to add channel header text for any channel
|
||||||
|
// other than a DM or a GM or a Bot DM based on user's permission
|
||||||
|
return (
|
||||||
|
<ChannelPermissionGate
|
||||||
|
channelId={props.channel.id}
|
||||||
|
teamId={props.teamId}
|
||||||
|
permissions={[
|
||||||
|
isPrivateChannel ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AddChannelHeaderTextButton channel={props.channel}/>
|
||||||
|
</ChannelPermissionGate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddChannelHeaderTextButton({channel}: {channel: Channel}) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||||
|
dialogType: EditChannelHeaderModal,
|
||||||
|
dialogProps: {channel},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className='header-placeholder style--none'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.headerText.addNewButton'
|
||||||
|
defaultMessage='Add a channel header'
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
className='icon icon-pencil-outline edit-icon'
|
||||||
|
aria-hidden={true}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
@use 'utils/variables';
|
||||||
|
|
||||||
|
.channel-header-text-popover-floating-overlay {
|
||||||
|
z-index: variables.$z-index-popover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-header-text-popover {
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: calc(100vw - 10px);
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
background: var(--center-channel-bg);
|
||||||
|
box-shadow: var(--elevation-4);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
.markdown__list {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-inline-img {
|
||||||
|
max-height: 18px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
height: 18px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hover {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(variables.$black, 0.1), 0 2px 2px 0 rgba(variables.$black, 0.1);
|
||||||
|
transition: all 0.1s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--no-border {
|
||||||
|
border: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.markdown__paragraph-inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.broken-image {
|
||||||
|
max-height: 18px;
|
||||||
|
border-radius: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-loaded-container {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview__button {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.markdown-inline-img {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoticon {
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul + p,
|
||||||
|
ol + p {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p + ul,
|
||||||
|
p + ol {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: 0;
|
||||||
|
left: 2px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: normal;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {
|
||||||
|
autoUpdate,
|
||||||
|
useDismiss,
|
||||||
|
safePolygon,
|
||||||
|
useFocus,
|
||||||
|
useHover,
|
||||||
|
useTransitionStyles,
|
||||||
|
useInteractions,
|
||||||
|
useRole,
|
||||||
|
useMergeRefs,
|
||||||
|
useFloating,
|
||||||
|
FloatingPortal,
|
||||||
|
FloatingOverlay,
|
||||||
|
useClick,
|
||||||
|
offset,
|
||||||
|
} from '@floating-ui/react';
|
||||||
|
import React, {useMemo, useRef, useState} from 'react';
|
||||||
|
import type {MouseEvent} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
|
import Markdown from 'components/markdown';
|
||||||
|
|
||||||
|
import {OverlaysTimings, OverlayTransitionStyles, RootHtmlPortalId} from 'utils/constants';
|
||||||
|
import type {ChannelNamesMap} from 'utils/text_formatting';
|
||||||
|
import {handleFormattedTextClick} from 'utils/utils';
|
||||||
|
|
||||||
|
import './channel_header_text_popover.scss';
|
||||||
|
|
||||||
|
const TEXT_IN_HEADER_MARKDOWN_OPTIONS = {singleline: true};
|
||||||
|
const TEXT_IN_POPOVER_MARKDOWN_OPTIONS = {singleline: false};
|
||||||
|
const MENTION_MARKDOWN_OPTIONS = {mentionHighlight: false, atMentions: true};
|
||||||
|
const IMAGE_MARKDOWN_OPTIONS = {hideUtilities: true};
|
||||||
|
|
||||||
|
const TRANSITION_STYLE_PROPS = {
|
||||||
|
duration: {
|
||||||
|
open: OverlaysTimings.FADE_IN_DURATION,
|
||||||
|
close: OverlaysTimings.FADE_OUT_DURATION,
|
||||||
|
},
|
||||||
|
initial: OverlayTransitionStyles.START,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PADDING_Y_OF_POPOVER = 6; // padding top & bottom of .channel-header-text-popover in channel_header_text_popover.scss
|
||||||
|
const PADDING_X_OF_POPOVER = 8; // padding right & left of .channel-header-text-popover in channel_header_text_popover.scss
|
||||||
|
const BORDER_WIDTH_OF_POPOVER = 1; // border of .channel-header-text-popover in channel_header_text_popover.scss
|
||||||
|
|
||||||
|
const HEIGHT_OF_HEADER_TEXT = 24; // height of .header-description__text in _headers.scss
|
||||||
|
const SHIFT_UP_OF_POPOVER = -((HEIGHT_OF_HEADER_TEXT + PADDING_Y_OF_POPOVER) - (2 * BORDER_WIDTH_OF_POPOVER));
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
channelMentionsNameMap?: ChannelNamesMap;
|
||||||
|
}
|
||||||
|
export function ChannelHeaderTextPopover(props: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const currentRelativeTeamUrl = useSelector(getCurrentRelativeTeamUrl);
|
||||||
|
|
||||||
|
const rootElementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const isTextOverflowing = checkIfTextIsOverflowing(rootElementRef?.current, props.text);
|
||||||
|
|
||||||
|
const markdownOptions = useMemo(() => {
|
||||||
|
const inHeader = {
|
||||||
|
...TEXT_IN_HEADER_MARKDOWN_OPTIONS,
|
||||||
|
...MENTION_MARKDOWN_OPTIONS,
|
||||||
|
channelNamesMap: props.channelMentionsNameMap,
|
||||||
|
};
|
||||||
|
const inPopover = {
|
||||||
|
...TEXT_IN_POPOVER_MARKDOWN_OPTIONS,
|
||||||
|
...MENTION_MARKDOWN_OPTIONS,
|
||||||
|
channelNamesMap: props.channelMentionsNameMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
inHeader,
|
||||||
|
inPopover,
|
||||||
|
};
|
||||||
|
}, [props.channelMentionsNameMap]);
|
||||||
|
|
||||||
|
const [isPopoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
|
||||||
|
const {refs: {setReference, setFloating}, floatingStyles, context: floatingContext} = useFloating({
|
||||||
|
open: isTextOverflowing ? isPopoverOpen : false,
|
||||||
|
onOpenChange: setPopoverOpen,
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
middleware: [
|
||||||
|
offset(SHIFT_UP_OF_POPOVER),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const {isMounted, styles: transitionStyles} = useTransitionStyles(
|
||||||
|
floatingContext,
|
||||||
|
TRANSITION_STYLE_PROPS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hover = useHover(floatingContext, {
|
||||||
|
enabled: isTextOverflowing,
|
||||||
|
handleClose: safePolygon({
|
||||||
|
requireIntent: false,
|
||||||
|
blockPointerEvents: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const focus = useFocus(floatingContext);
|
||||||
|
const dismiss = useDismiss(floatingContext);
|
||||||
|
const click = useClick(floatingContext);
|
||||||
|
const role = useRole(floatingContext, {role: 'tooltip'});
|
||||||
|
|
||||||
|
const {getReferenceProps, getFloatingProps} = useInteractions([hover, focus, click, dismiss, role]);
|
||||||
|
|
||||||
|
const rootRef = useMergeRefs([rootElementRef, setReference]);
|
||||||
|
|
||||||
|
const maxWidthOfPopover = getMaxWidthOfPopover(rootElementRef?.current);
|
||||||
|
|
||||||
|
// This action processes clicks on formatted text elements like hashtags, user mentions,
|
||||||
|
// channel mentions, etc. while also allowing other elements to function as is such as external links etc
|
||||||
|
function handleClick(event: MouseEvent<HTMLDivElement>) {
|
||||||
|
dispatch(handleFormattedTextClick(event, currentRelativeTeamUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={rootRef}
|
||||||
|
className='header-description__text'
|
||||||
|
{...getReferenceProps()}
|
||||||
|
>
|
||||||
|
<Markdown
|
||||||
|
message={props.text}
|
||||||
|
options={markdownOptions.inHeader}
|
||||||
|
imageProps={IMAGE_MARKDOWN_OPTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isMounted && (
|
||||||
|
<FloatingPortal id={RootHtmlPortalId}>
|
||||||
|
<FloatingOverlay
|
||||||
|
className='channel-header-text-popover-floating-overlay'
|
||||||
|
lockScroll={true}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div
|
||||||
|
ref={setFloating}
|
||||||
|
className='channel-header-text-popover'
|
||||||
|
style={{
|
||||||
|
maxWidth: maxWidthOfPopover,
|
||||||
|
...floatingStyles,
|
||||||
|
...transitionStyles,
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
<Markdown
|
||||||
|
message={props.text}
|
||||||
|
options={markdownOptions.inPopover}
|
||||||
|
imageProps={IMAGE_MARKDOWN_OPTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FloatingOverlay>
|
||||||
|
</FloatingPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfTextIsOverflowing(elem: HTMLDivElement | null, text: string): boolean {
|
||||||
|
if (!elem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.match(/\n{2,}/g)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elem.scrollWidth === elem.clientWidth && elem.scrollHeight === elem.clientHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return elem.scrollWidth > elem.clientWidth || elem.scrollHeight > elem.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxWidthOfPopover(elem: HTMLDivElement | null): string | number {
|
||||||
|
if (!elem) {
|
||||||
|
return 'inherit';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (elem.clientWidth) + ((2 * PADDING_X_OF_POPOVER) + (2 * BORDER_WIDTH_OF_POPOVER));
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {ConnectedProps} from 'react-redux';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import {withRouter} from 'react-router-dom';
|
import {withRouter} from 'react-router-dom';
|
||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators} from 'redux';
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
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 {getCurrentRelativeTeamUrl, getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {
|
import {
|
||||||
displayLastActiveLabel,
|
displayLastActiveLabel,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
@@ -29,20 +30,15 @@ import {
|
|||||||
} from 'mattermost-redux/selectors/entities/users';
|
} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
|
import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||||
|
|
||||||
import {goToLastViewedChannel} from 'actions/views/channel';
|
|
||||||
import {openModal, closeModal} from 'actions/views/modals';
|
|
||||||
import {
|
import {
|
||||||
showPinnedPosts,
|
showPinnedPosts,
|
||||||
showChannelFiles,
|
showChannelFiles,
|
||||||
closeRightHandSide,
|
closeRightHandSide,
|
||||||
showChannelMembers,
|
showChannelMembers,
|
||||||
} from 'actions/views/rhs';
|
} from 'actions/views/rhs';
|
||||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
import {getRhsState} from 'selectors/rhs';
|
||||||
import {getAnnouncementBarCount} from 'selectors/views/announcement_bar';
|
|
||||||
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired} from 'selectors/views/custom_status';
|
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||||
import {isModalOpen} from 'selectors/views/modals';
|
|
||||||
|
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
|
||||||
import {isFileAttachmentsEnabled} from 'utils/file_utils';
|
import {isFileAttachmentsEnabled} from 'utils/file_utils';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
@@ -57,8 +53,6 @@ function makeMapStateToProps() {
|
|||||||
return function mapStateToProps(state: GlobalState) {
|
return function mapStateToProps(state: GlobalState) {
|
||||||
const channel = getCurrentChannel(state);
|
const channel = getCurrentChannel(state);
|
||||||
const user = getCurrentUser(state);
|
const user = getCurrentUser(state);
|
||||||
const teams = getMyTeams(state);
|
|
||||||
const hasMoreThanOneTeam = teams.length > 1;
|
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
|
|
||||||
let dmUser;
|
let dmUser;
|
||||||
@@ -91,15 +85,9 @@ function makeMapStateToProps() {
|
|||||||
dmUser,
|
dmUser,
|
||||||
gmMembers,
|
gmMembers,
|
||||||
rhsState: getRhsState(state),
|
rhsState: getRhsState(state),
|
||||||
rhsOpen: getIsRhsOpen(state),
|
isChannelMuted: isCurrentChannelMuted(state),
|
||||||
isReadOnly: false,
|
|
||||||
isMuted: isCurrentChannelMuted(state),
|
|
||||||
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
|
|
||||||
hasGuests: stats ? stats.guest_count > 0 : false,
|
hasGuests: stats ? stats.guest_count > 0 : false,
|
||||||
pinnedPostsCount: stats?.pinnedpost_count || 0,
|
pinnedPostsCount: stats?.pinnedpost_count || 0,
|
||||||
hasMoreThanOneTeam,
|
|
||||||
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
|
||||||
announcementBarCount: getAnnouncementBarCount(state),
|
|
||||||
customStatus,
|
customStatus,
|
||||||
isCustomStatusEnabled: isCustomStatusEnabled(state),
|
isCustomStatusEnabled: isCustomStatusEnabled(state),
|
||||||
isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
|
isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
|
||||||
@@ -119,11 +107,12 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|||||||
closeRightHandSide,
|
closeRightHandSide,
|
||||||
getCustomEmojisInText,
|
getCustomEmojisInText,
|
||||||
updateChannelNotifyProps,
|
updateChannelNotifyProps,
|
||||||
goToLastViewedChannel,
|
|
||||||
openModal,
|
|
||||||
closeModal,
|
|
||||||
showChannelMembers,
|
showChannelMembers,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter<any, any>(connect(makeMapStateToProps, mapDispatchToProps)(ChannelHeader));
|
const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
export default withRouter(connector(ChannelHeader));
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
@use 'utils/variables';
|
||||||
@use 'utils/mixins';
|
@use 'utils/mixins';
|
||||||
|
|
||||||
.user-profile-popover-floating-overlay {
|
.user-profile-popover-floating-overlay {
|
||||||
// 99 being the z-index of the global header
|
z-index: variables.$z-index-popover;
|
||||||
// 1060 being the z-index of the user group popover
|
|
||||||
z-index: 1070;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$profilePopoverWidth: 240px;
|
$profilePopoverWidth: 240px;
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ export function ProfilePopoverController<TriggerComponentType = HTMLSpanElement>
|
|||||||
{isMounted && (
|
{isMounted && (
|
||||||
<FloatingPortal id={RootHtmlPortalId}>
|
<FloatingPortal id={RootHtmlPortalId}>
|
||||||
<FloatingOverlay
|
<FloatingOverlay
|
||||||
id='user-profile-popover-floating-overlay'
|
|
||||||
className='user-profile-popover-floating-overlay'
|
className='user-profile-popover-floating-overlay'
|
||||||
lockScroll={true}
|
lockScroll={true}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3183,7 +3183,6 @@
|
|||||||
"channel_bookmarks.editBookmarkLabel": "Bookmark menu",
|
"channel_bookmarks.editBookmarkLabel": "Bookmark menu",
|
||||||
"channel_bookmarks.open": "Open",
|
"channel_bookmarks.open": "Open",
|
||||||
"channel_groups": "{channel} Groups",
|
"channel_groups": "{channel} Groups",
|
||||||
"channel_header.addChannelHeader": "Add a channel header",
|
|
||||||
"channel_header.channelFiles": "Channel files",
|
"channel_header.channelFiles": "Channel files",
|
||||||
"channel_header.channelHasGuests": "Channel has guests",
|
"channel_header.channelHasGuests": "Channel has guests",
|
||||||
"channel_header.channelMembers": "Members",
|
"channel_header.channelMembers": "Members",
|
||||||
@@ -3191,9 +3190,9 @@
|
|||||||
"channel_header.convert": "Convert to Private Channel",
|
"channel_header.convert": "Convert to Private Channel",
|
||||||
"channel_header.delete": "Archive Channel",
|
"channel_header.delete": "Archive Channel",
|
||||||
"channel_header.directchannel.you": "{displayname} (you) ",
|
"channel_header.directchannel.you": "{displayname} (you) ",
|
||||||
"channel_header.editLink": "Edit",
|
|
||||||
"channel_header.flagged": "Saved messages",
|
"channel_header.flagged": "Saved messages",
|
||||||
"channel_header.groupMessageHasGuests": "This group message has guests",
|
"channel_header.groupMessageHasGuests": "This group message has guests",
|
||||||
|
"channel_header.headerText.addNewButton": "Add a channel header",
|
||||||
"channel_header.lastActive": "Last online {timestamp}",
|
"channel_header.lastActive": "Last online {timestamp}",
|
||||||
"channel_header.lastOnline": "Last online {timestamp}",
|
"channel_header.lastOnline": "Last online {timestamp}",
|
||||||
"channel_header.leave": "Leave Channel",
|
"channel_header.leave": "Leave Channel",
|
||||||
@@ -3912,7 +3911,6 @@
|
|||||||
"generic_icons.member": "Member Icon",
|
"generic_icons.member": "Member Icon",
|
||||||
"generic_icons.mention": "Mention Icon",
|
"generic_icons.mention": "Mention Icon",
|
||||||
"generic_icons.menu": "Menu Icon",
|
"generic_icons.menu": "Menu Icon",
|
||||||
"generic_icons.muted": "Muted Icon",
|
|
||||||
"generic_icons.next": "Next Icon",
|
"generic_icons.next": "Next Icon",
|
||||||
"generic_icons.pin": "Pin Icon",
|
"generic_icons.pin": "Pin Icon",
|
||||||
"generic_icons.plugin": "Plugin Icon",
|
"generic_icons.plugin": "Plugin Icon",
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
@use "utils/functions";
|
@use "utils/functions";
|
||||||
|
|
||||||
.header-popover-text-measurer {
|
|
||||||
position: absolute;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
visibility: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
@@ -21,54 +13,6 @@
|
|||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.channel-header__popover {
|
|
||||||
z-index: 999;
|
|
||||||
min-width: 200px;
|
|
||||||
margin-left: 3px;
|
|
||||||
box-shadow: var(--elevation-4);
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
&.chanel-header__popover--lhs_offset {
|
|
||||||
margin-left: 68px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-content {
|
|
||||||
padding: 5px 8px 8px 8px;
|
|
||||||
|
|
||||||
.markdown__list {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
white-space: normal;
|
|
||||||
word-break: normal;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-preview__button {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
.markdown-inline-img {
|
|
||||||
margin: 0 2px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoticon {
|
|
||||||
min-width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
min-height: 18px;
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom {
|
|
||||||
margin-top: -25px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom,
|
&.bottom,
|
||||||
&.right,
|
&.right,
|
||||||
&.top,
|
&.top,
|
||||||
@@ -193,13 +137,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-header__info {
|
|
||||||
.popover-content {
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 250px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-popover {
|
.user-popover {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
.channel-header {
|
.channel-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 15;
|
z-index: variables.$z-index-channel-header;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 56px;
|
max-height: 56px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -112,14 +112,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 24px 0 2px;
|
padding: 0 24px 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown__paragraph-inline {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-header_plugin-dropdown {
|
.channel-header_plugin-dropdown {
|
||||||
@@ -390,19 +382,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0 0 0 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
top: 0;
|
|
||||||
left: 2px;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-placeholder {
|
.header-placeholder {
|
||||||
display: none;
|
display: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -446,6 +425,8 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
@@ -462,6 +443,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&>blockquote {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: 0;
|
||||||
|
left: 2px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.emoticon {
|
.emoticon {
|
||||||
width: 18.8px;
|
width: 18.8px;
|
||||||
min-width: 18.8px;
|
min-width: 18.8px;
|
||||||
@@ -470,6 +465,17 @@
|
|||||||
min-height: 18.8px;
|
min-height: 18.8px;
|
||||||
max-height: 18.8px;
|
max-height: 18.8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&>p.markdown__paragraph-inline:first-child {
|
||||||
|
display: inline;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>p.markdown__paragraph-inline:not(:first-child) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown__heading {
|
.markdown__heading {
|
||||||
@@ -565,7 +571,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-header__popover,
|
|
||||||
.navbar__popover {
|
.navbar__popover {
|
||||||
p {
|
p {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -147,49 +147,6 @@ h6.markdown__heading {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#channelHeaderDescription,
|
|
||||||
#header-popover {
|
|
||||||
.markdown-inline-img {
|
|
||||||
max-height: 18px;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin: 2px;
|
|
||||||
-moz-force-broken-image-icon: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-inline-img--loading {
|
|
||||||
height: 18px;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-inline-img--hover {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
z-index: 2;
|
|
||||||
box-shadow: 0 2px 2px 0 rgba(variables.$black, 0.1), 0 2px 2px 0 rgba(variables.$black, 0.1);
|
|
||||||
transition: all 0.1s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-inline-img--no-border {
|
|
||||||
border: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.markdown__paragraph-inline {
|
|
||||||
display: inline-block;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.broken-image {
|
|
||||||
max-height: 18px;
|
|
||||||
border-radius: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-loaded-container {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-code {
|
.post-code {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@@ -763,66 +763,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-popover {
|
|
||||||
position: fixed;
|
|
||||||
top: 40px !important;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: calc(100% - 50px);
|
|
||||||
border: none;
|
|
||||||
background: functions.alpha-color(variables.$black, 0.9);
|
|
||||||
box-shadow: none;
|
|
||||||
transform: translate3d(0, 100%, 0);
|
|
||||||
transition: all 0.35s ease;
|
|
||||||
|
|
||||||
&.in {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #0091ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-content {
|
|
||||||
padding: 15px 20px 100px;
|
|
||||||
color: variables.$white;
|
|
||||||
font-size: 15px;
|
|
||||||
|
|
||||||
>div {
|
|
||||||
&:first-child {
|
|
||||||
overflow: auto;
|
|
||||||
height: calc(100vh - 150px);
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 25px;
|
|
||||||
left: 50%;
|
|
||||||
display: block;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
padding-top: 13px;
|
|
||||||
border: 1px solid variables.$white;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-left: -15px;
|
|
||||||
color: variables.$white;
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
|
||||||
font-size: 23px;
|
|
||||||
font-weight: 200;
|
|
||||||
line-height: 0;
|
|
||||||
opacity: 1;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-div {
|
.video-div {
|
||||||
&.embed-responsive-item {
|
&.embed-responsive-item {
|
||||||
iframe {
|
iframe {
|
||||||
|
|||||||
@@ -119,11 +119,6 @@
|
|||||||
width: 274px;
|
width: 274px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-header__popover {
|
|
||||||
top: 44px !important;
|
|
||||||
left: 236px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-sm-auto {
|
.col-sm-auto {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,3 +42,4 @@ $elevation-6: var(--elevation-6);
|
|||||||
$z-index-popover: 1070;
|
$z-index-popover: 1070;
|
||||||
$z-index-user-group-popover: 1060;
|
$z-index-user-group-popover: 1060;
|
||||||
$z-index-global-header: 99;
|
$z-index-global-header: 99;
|
||||||
|
$z-index-channel-header: 15;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user