[MM-62384] Move ChannelHeaderPopover from BootstrapOverlay/Popper to Floating-UI (#29820)

Этот коммит содержится в:
M-ZubairAhmed
2025-01-14 17:19:55 +05:30
коммит произвёл GitHub
родитель 7cf040655f
Коммит 25a6501e10
20 изменённых файлов: 1264 добавлений и 1980 удалений

Просмотреть файл

@@ -150,12 +150,12 @@ describe('Direct Message', () => {
cy.get('#edit_textbox').type(message).type('{enter}').wait(TIMEOUTS.HALF_SEC);
// # 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
cy.wait(TIMEOUTS.HALF_SEC);
cy.get('#header-popover .popover-content').should('be.visible');
cy.get('#header-popover').find('.popover-content').should(($el) => {
cy.get('.channel-header-text-popover').should('be.visible');
cy.get('.channel-header-text-popover').should(($el) => {
expect($el.get(0).innerText).to.eq(expectedMessage);
});
});

Просмотреть файл

@@ -58,12 +58,13 @@ describe('Header', () => {
updateAndVerifyChannelHeader('>', header);
// # Click the header to see the whole text
cy.get('#channelHeaderDescription').click();
// # Hover on channel header
cy.get('#channelHeaderDescription .header-description__text').trigger('mouseenter');
// * Check that no ellipsis is present
cy.get('#header-popover > div.popover-content').
should('have.html', `<span><blockquote>\n<p>${header}</p>\n</blockquote></span>`);
cy.get('.channel-header-text-popover').should(($el) => {
expect($el.get(0).innerText).to.eq(header);
});
cy.apiSaveMessageDisplayPreference('clean');
});
@@ -75,12 +76,14 @@ function updateAndVerifyChannelHeader(prefix, header) {
// * Should render blockquote if it starts with ">"
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
cy.get('#channelHeaderDescription > .header-description__text').find('p').
should('have.text', header).
cy.get('.header-description__text').
should('include.text', header).
and('have.css', 'overflow', 'hidden').
and('have.css', 'text-overflow', 'ellipsis');
}

Просмотреть файл

@@ -47,28 +47,36 @@ describe('Multi-user group header', () => {
cy.apiLogin(testUser);
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
// * no channel header is set
cy.get('button.header-placeholder').invoke('show').trigger('mouseover');
cy.contains('#channelHeaderDescription button span', 'Add a channel header').should('be.visible');
const header = 'peace and progress';
// # click add a channel heander
cy.findByRoleExtended('button', {name: 'Add a channel header'}).should('be.visible').click();
// * Verify that no channel header is set
cy.get('#channel-header').within(() => {
cy.findByText('Add a channel header').should('not.be.visible');
});
// # type a header
const header = 'this is a header!';
// # Force click on button which is hidden and shows on hover
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('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
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');
// * 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);
// * channel is marked as unread for other user
// * Channel is marked as unread for other user
cy.apiLogout();
cy.apiLogin(userList[0]);
cy.visit(`/${testTeam.name}/channels/town-square`);
@@ -81,18 +89,22 @@ describe('Multi-user group header', () => {
cy.apiLogin(testUser);
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
// * verify header is set
cy.contains('#channelHeaderDescription button span', 'Add a channel header').should('not.exist');
// * Verify that channel header is set
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);
// * text appears at the top
cy.contains('#channelHeaderDescription span.header-description__text p', header);
// * Header text appears at the top
cy.get('#channel-header').within(() => {
cy.findByText(header).should('be.visible');
});
checkSystemMessage('updated the channel header');
// * channel is marked as unread for other users
// * Channel is marked as unread for other users
cy.apiLogout();
cy.apiLogin(userList[0]);
cy.visit(`/${testTeam.name}/channels/town-square`);
@@ -101,35 +113,43 @@ describe('Multi-user group header', () => {
});
it('MM-T473_2 Edit GM channel header', () => {
// # open existing GM
// # Open existing GM
cy.apiLogin(testUser);
cy.visit(`/${testTeam.name}/channels/${groupChannel.name}`);
// * verify header is set
cy.contains('#channelHeaderDescription button span', 'Add a channel header').should('not.exist');
// * Verify that channel header is set
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);
cy.get('#channelHeaderDescription').find('.header-description__text').
find('.mention-link').
should('be.visible').and('have.text', `@${testUser.username}`);
cy.get('#channelHeaderDescription').find('.header-description__text').
find('.mention--highlight').
should('not.exist');
// * Header text appears at the top
cy.get('#channel-header').within(() => {
// * Verify mention is present
cy.get('.mention-link').should('be.visible').and('have.text', `@${testUser.username}`);
// * Verify its not highlighted
cy.get('.mention--highlight').should('not.exist');
});
});
const editHeader = (header) => {
function editHeader(header) {
// # Click edit conversation header
cy.uiOpenChannelMenu('Edit Conversation Header');
// # type new header
cy.get('#editChannelHeaderModalLabel').should('be.visible');
cy.get('textarea#edit_textbox').should('be.visible').clear().type(`${header}{enter}`);
cy.get('#editChannelHeaderModalLabel').should('not.exist'); // wait for modal to disappear
};
// * Verify the modal open to add header
cy.get('#editChannelHeaderModalLabel').should('be.visible').wait(TIMEOUTS.ONE_SEC);
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
cy.getLastPostId().then((id) => {
cy.get(`#postMessageText_${id}`).should('contain', message);
@@ -138,5 +158,5 @@ describe('Multi-user group header', () => {
cy.get(`#delete_post_${id}`);
});
});
};
}
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -2,48 +2,56 @@
// See LICENSE.txt for license information.
import React from 'react';
import type {ComponentProps} from 'react';
import type {ChannelType} from '@mattermost/types/channels';
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 Markdown from 'components/markdown';
import type {MockIntl} from 'tests/helpers/intl-test-helper';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import Constants, {RHSStates} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import ChannelHeader from './channel_header';
import type {Props} from './channel_header';
describe('components/ChannelHeader', () => {
const baseProps: ComponentProps<typeof ChannelHeader> = {
const baseProps: Props = {
actions: {
showPinnedPosts: jest.fn(),
showChannelFiles: jest.fn(),
closeRightHandSide: jest.fn(),
openModal: 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({}),
currentRelativeTeamUrl: '',
isCustomStatusEnabled: false,
isCustomStatusExpired: false,
isFileAttachmentsEnabled: true,
lastActivityTimestamp: 1632146562846,
isLastActiveEnabled: true,
memberCount: 2,
dmUser: undefined,
gmMembers: undefined,
rhsState: RHSStates.CHANNEL_INFO,
isChannelMuted: false,
hasGuests: false,
pinnedPostsCount: 0,
customStatus: undefined,
timestampUnits: [
'now',
'minute',
'hour',
],
hideGuestTags: false,
intl: {
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
} as MockIntl,
};
const populatedProps = {
@@ -140,7 +148,7 @@ describe('components/ChannelHeader', () => {
test('should render correct menu when muted', () => {
const props = {
...populatedProps,
isMuted: true,
isChannelMuted: true,
};
const wrapper = shallowWithIntl(
@@ -152,7 +160,7 @@ describe('components/ChannelHeader', () => {
test('should unmute the channel when mute icon is clicked', () => {
const props = {
...populatedProps,
isMuted: true,
isChannelMuted: true,
};
const wrapper = shallowWithIntl(
@@ -227,30 +235,6 @@ describe('components/ChannelHeader', () => {
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,

Просмотреть файл

@@ -4,125 +4,38 @@
import classNames from 'classnames';
import React 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 type {IntlShape} 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 type {WrappedComponentProps} from 'react-intl';
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
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 Popover from 'components/widgets/popover';
import WithTooltip from 'components/with_tooltip';
import CallButton from 'plugins/call_button';
import ChannelHeaderPlug from 'plugins/channel_header_plug';
import {
Constants,
ModalIdentifiers,
NotificationLevels,
RHSStates,
} from 'utils/constants';
import {handleFormattedTextClick, isEmptyObject} from 'utils/utils';
import type {ModalData} from 'types/actions';
import type {RhsState} from 'types/store/rhs';
import {isEmptyObject} from 'utils/utils';
import ChannelHeaderText from './channel_header_text';
import ChannelHeaderTitle from './channel_header_title';
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};
import type {PropsFromRedux} from './index';
export type OverlayTrigger = BaseOverlayTrigger & {
hide: () => void;
};
export type Props = WrappedComponentProps & PropsFromRedux;
export type 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> {
class ChannelHeader extends React.PureComponent<Props> {
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) {
super(props);
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() {
@@ -137,8 +50,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
}
}
handleClose = () => this.props.actions.goToLastViewedChannel();
unmute = () => {
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);
};
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) {
@@ -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 = () => {
if (this.props.rhsState === RHSStates.CHANNEL_MEMBERS) {
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 = () => {
const {customStatus, isCustomStatusEnabled, isCustomStatusExpired} = this.props;
const isStatusSet = !isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
@@ -259,8 +118,7 @@ class ChannelHeader extends React.PureComponent<Props, State> {
gmMembers,
channel,
channelMember,
isMuted: channelMuted,
isReadOnly,
isChannelMuted,
dmUser,
rhsState,
hasGuests,
@@ -270,7 +128,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
return null;
}
const {formatMessage} = this.props.intl;
const ariaLabelChannelHeader = this.props.intl.formatMessage({id: 'accessibility.sections.channelHeader', defaultMessage: 'channel header region'});
let hasGuestsText: ReactNode = '';
@@ -287,7 +144,6 @@ class ChannelHeader extends React.PureComponent<Props, State> {
);
}
const channelIsArchived = channel.delete_at !== 0;
if (isEmptyObject(channel) ||
isEmptyObject(channelMember) ||
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 isGroup = (channel.type === Constants.GM_CHANNEL);
const isPrivate = (channel.type === Constants.PRIVATE_CHANNEL);
if (isGroup) {
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;
if (channelMuted) {
if (isChannelMuted) {
muteTrigger = (
<WithTooltip
title={
@@ -579,9 +304,12 @@ class ChannelHeader extends React.PureComponent<Props, State> {
id='toggleMute'
onClick={this.unmute}
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>
</WithTooltip>
);
@@ -627,7 +355,18 @@ class ChannelHeader extends React.PureComponent<Props, State> {
</HeaderIconWrapper>
}
</div>
{headerTextContainer}
<div
id='channelHeaderDescription'
className='channel-header__description'
>
{dmHeaderTextStatus}
{hasGuestsText}
<ChannelHeaderText
teamId={teamId}
channel={channel}
dmUser={dmUser}
/>
</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.
// See LICENSE.txt for license information.
import type {ConnectedProps} from 'react-redux';
import {connect} from 'react-redux';
import {withRouter} from 'react-router-dom';
import {bindActionCreators} from 'redux';
@@ -18,7 +19,7 @@ import {
getCurrentChannelStats,
} from 'mattermost-redux/selectors/entities/channels';
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 {
displayLastActiveLabel,
getCurrentUser,
@@ -29,20 +30,15 @@ import {
} 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 {getIsRhsOpen, getRhsState} from 'selectors/rhs';
import {getAnnouncementBarCount} from 'selectors/views/announcement_bar';
import {getRhsState} from 'selectors/rhs';
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 type {GlobalState} from 'types/store';
@@ -57,8 +53,6 @@ function makeMapStateToProps() {
return function mapStateToProps(state: GlobalState) {
const channel = getCurrentChannel(state);
const user = getCurrentUser(state);
const teams = getMyTeams(state);
const hasMoreThanOneTeam = teams.length > 1;
const config = getConfig(state);
let dmUser;
@@ -91,15 +85,9 @@ function makeMapStateToProps() {
dmUser,
gmMembers,
rhsState: getRhsState(state),
rhsOpen: getIsRhsOpen(state),
isReadOnly: false,
isMuted: isCurrentChannelMuted(state),
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
isChannelMuted: isCurrentChannelMuted(state),
hasGuests: stats ? stats.guest_count > 0 : false,
pinnedPostsCount: stats?.pinnedpost_count || 0,
hasMoreThanOneTeam,
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
announcementBarCount: getAnnouncementBarCount(state),
customStatus,
isCustomStatusEnabled: isCustomStatusEnabled(state),
isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
@@ -119,11 +107,12 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
closeRightHandSide,
getCustomEmojisInText,
updateChannelNotifyProps,
goToLastViewedChannel,
openModal,
closeModal,
showChannelMembers,
}, 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';
.user-profile-popover-floating-overlay {
// 99 being the z-index of the global header
// 1060 being the z-index of the user group popover
z-index: 1070;
z-index: variables.$z-index-popover;
}
$profilePopoverWidth: 240px;

Просмотреть файл

@@ -118,7 +118,6 @@ export function ProfilePopoverController<TriggerComponentType = HTMLSpanElement>
{isMounted && (
<FloatingPortal id={RootHtmlPortalId}>
<FloatingOverlay
id='user-profile-popover-floating-overlay'
className='user-profile-popover-floating-overlay'
lockScroll={true}
>

Просмотреть файл

@@ -3183,7 +3183,6 @@
"channel_bookmarks.editBookmarkLabel": "Bookmark menu",
"channel_bookmarks.open": "Open",
"channel_groups": "{channel} Groups",
"channel_header.addChannelHeader": "Add a channel header",
"channel_header.channelFiles": "Channel files",
"channel_header.channelHasGuests": "Channel has guests",
"channel_header.channelMembers": "Members",
@@ -3191,9 +3190,9 @@
"channel_header.convert": "Convert to Private Channel",
"channel_header.delete": "Archive Channel",
"channel_header.directchannel.you": "{displayname} (you) ",
"channel_header.editLink": "Edit",
"channel_header.flagged": "Saved messages",
"channel_header.groupMessageHasGuests": "This group message has guests",
"channel_header.headerText.addNewButton": "Add a channel header",
"channel_header.lastActive": "Last online {timestamp}",
"channel_header.lastOnline": "Last online {timestamp}",
"channel_header.leave": "Leave Channel",
@@ -3912,7 +3911,6 @@
"generic_icons.member": "Member Icon",
"generic_icons.mention": "Mention Icon",
"generic_icons.menu": "Menu Icon",
"generic_icons.muted": "Muted Icon",
"generic_icons.next": "Next Icon",
"generic_icons.pin": "Pin Icon",
"generic_icons.plugin": "Plugin Icon",

Просмотреть файл

@@ -1,13 +1,5 @@
@use "utils/functions";
.header-popover-text-measurer {
position: absolute;
width: auto;
height: auto;
visibility: hidden;
white-space: nowrap;
}
.popover {
padding: 0;
border-radius: var(--radius-s);
@@ -21,54 +13,6 @@
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,
&.right,
&.top,
@@ -193,13 +137,6 @@
}
}
.channel-header__info {
.popover-content {
overflow: auto;
max-height: 250px;
}
}
.user-popover {
display: inline-block;
cursor: pointer;

Просмотреть файл

@@ -4,7 +4,7 @@
.channel-header {
position: relative;
z-index: 15;
z-index: variables.$z-index-channel-header;
width: 100%;
max-height: 56px;
padding: 12px;
@@ -112,14 +112,6 @@
flex: 1;
padding: 0 24px 0 2px;
}
.markdown__paragraph-inline {
display: inline-block;
overflow: hidden;
width: 100%;
margin: 0;
text-overflow: ellipsis;
}
}
.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 {
display: none;
overflow: hidden;
@@ -446,6 +425,8 @@
overflow: hidden;
height: 24px;
line-height: inherit;
text-overflow: ellipsis;
white-space: nowrap;
ul {
&: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 {
width: 18.8px;
min-width: 18.8px;
@@ -470,6 +465,17 @@
min-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 {
@@ -565,7 +571,6 @@
}
}
.channel-header__popover,
.navbar__popover {
p {
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 {
position: relative;
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 {
&.embed-responsive-item {
iframe {

Просмотреть файл

@@ -119,11 +119,6 @@
width: 274px;
}
.channel-header__popover {
top: 44px !important;
left: 236px !important;
}
.col-sm-auto {
float: left;
}

Просмотреть файл

@@ -42,3 +42,4 @@ $elevation-6: var(--elevation-6);
$z-index-popover: 1070;
$z-index-user-group-popover: 1060;
$z-index-global-header: 99;
$z-index-channel-header: 15;