diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js index 7e27cb8b4a..40c8ebb0ed 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js @@ -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); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js index 34542139a4..d3269482d4 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js @@ -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', `
\n

${header}

\n
`); + 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'); } diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js index b2fe2d8b34..ae03812617 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js @@ -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}`); }); }); - }; + } }); diff --git a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap index 71cfd63250..b8974259ce 100644 --- a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap +++ b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap @@ -84,7 +84,6 @@ exports[`components/ChannelHeader should match snapshot with last active display
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -362,98 +341,77 @@ exports[`components/ChannelHeader should match snapshot with no last active disp
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -572,7 +530,7 @@ exports[`components/ChannelHeader should render active channel files 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -793,7 +686,7 @@ exports[`components/ChannelHeader should render active flagged posts 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -1014,7 +842,7 @@ exports[`components/ChannelHeader should render active mentions posts 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -1235,7 +998,7 @@ exports[`components/ChannelHeader should render active pinned posts 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -1456,7 +1154,7 @@ exports[`components/ChannelHeader should render archived view 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -1672,12 +1305,13 @@ exports[`components/ChannelHeader should render correct menu when muted 1`] = ` } > @@ -1696,7 +1330,7 @@ exports[`components/ChannelHeader should render correct menu when muted 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -1917,7 +1486,7 @@ exports[`components/ChannelHeader should render not active channel files 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -2179,7 +1683,6 @@ exports[`components/ChannelHeader should render properly when custom status is e
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -2455,7 +1936,6 @@ exports[`components/ChannelHeader should render properly when custom status is s
-
- -
- - - - - - - - - - + } + teamId="team_id" + /> @@ -2708,7 +2166,7 @@ exports[`components/ChannelHeader should render properly when empty 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -2929,7 +2322,7 @@ exports[`components/ChannelHeader should render properly when populated 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -3150,7 +2478,7 @@ exports[`components/ChannelHeader should render properly when populated with cha className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -3397,7 +2655,7 @@ exports[`components/ChannelHeader should render shared view 1`] = ` className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
@@ -3620,7 +2814,7 @@ exports[`components/ChannelHeader should render the pinned icon with the pinned className="icon__text" id="channelMemberCountText" > - - + 2
-
- -
- - - - - - - - - - + } + teamId="team_id" + />
diff --git a/webapp/channels/src/components/channel_header/channel_header.test.tsx b/webapp/channels/src/components/channel_header/channel_header.test.tsx index 8827d7ad75..b686ae71a7 100644 --- a/webapp/channels/src/components/channel_header/channel_header.test.tsx +++ b/webapp/channels/src/components/channel_header/channel_header.test.tsx @@ -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 = { + 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( - , - ); - expect(wrapper.containsMatchingElement( - , - )).toEqual(true); - }); - test('should render the pinned icon with the pinned posts count', () => { const props = { ...populatedProps, diff --git a/webapp/channels/src/components/channel_header/channel_header.tsx b/webapp/channels/src/components/channel_header/channel_header.tsx index d12b45d2b0..136e094448 100644 --- a/webapp/channels/src/components/channel_header/channel_header.tsx +++ b/webapp/channels/src/components/channel_header/channel_header.tsx @@ -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) => void; - goToLastViewedChannel: () => void; - openModal:

(modalData: ModalData

) => 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 { +class ChannelHeader extends React.PureComponent { toggleFavoriteRef: RefObject; - headerDescriptionRef: RefObject; - headerPopoverTextMeasurerRef: RefObject; - headerOverlayRef: RefObject; - getHeaderMarkdownOptions: (channelNamesMap: Record) => Record; - getPopoverMarkdownOptions: (channelNamesMap: Record) => Record; 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) => ( - {...headerMarkdownOptions, channelNamesMap} - )); - this.getPopoverMarkdownOptions = memoizeResult((channelNamesMap: Record) => ( - {...popoverMarkdownOptions, channelNamesMap} - )); } componentDidMount() { @@ -137,8 +50,6 @@ class ChannelHeader extends React.PureComponent { } } - handleClose = () => this.props.actions.goToLastViewedChannel(); - unmute = () => { const {actions, channel, channelMember, currentUser} = this.props; @@ -150,17 +61,6 @@ class ChannelHeader extends React.PureComponent { 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) => { e.preventDefault(); if (this.props.rhsState === RHSStates.PIN) { @@ -178,45 +78,6 @@ class ChannelHeader extends React.PureComponent { } }; - 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 { } }; - handleFormattedTextClick = (e: MouseEvent) => 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 { gmMembers, channel, channelMember, - isMuted: channelMuted, - isReadOnly, + isChannelMuted, dmUser, rhsState, hasGuests, @@ -270,7 +128,6 @@ class ChannelHeader extends React.PureComponent { 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 { ); } - const channelIsArchived = channel.delete_at !== 0; if (isEmptyObject(channel) || isEmptyObject(channelMember) || isEmptyObject(currentUser) || @@ -299,11 +155,8 @@ class ChannelHeader extends React.PureComponent { ); } - 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 { ); } - let headerTextContainer; - const headerText = (isDirect && dmUser?.is_bot) ? dmUser.bot_description : channel.header; - if (headerText) { - const imageProps = { - hideUtilities: true, - }; - const popoverContent = ( - - - - - - ); - - headerTextContainer = ( -

- {dmHeaderTextStatus} - {hasGuestsText} -
- -
- this.showChannelHeaderPopover(headerText)} - onMouseOut={() => this.setState({showChannelHeaderPopover: false})} - ref={this.headerDescriptionRef} - > - this.setState({showChannelHeaderPopover: false})} - > - {popoverContent} - - - - -
- ); - } else { - let editMessage; - if (!isReadOnly && !channelIsArchived) { - if (isDirect || isGroup) { - if (!isDirect || !dmUser?.is_bot) { - editMessage = ( - - ); - } - } else { - editMessage = ( - - - - ); - } - } - headerTextContainer = ( -
- {dmHeaderTextStatus} - {hasGuestsText} - {editMessage} -
- ); - } - let muteTrigger; - if (channelMuted) { + if (isChannelMuted) { muteTrigger = ( { 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'})} > - + ); @@ -627,7 +355,18 @@ class ChannelHeader extends React.PureComponent {
} - {headerTextContainer} +
+ {dmHeaderTextStatus} + {hasGuestsText} + +
diff --git a/webapp/channels/src/components/channel_header/channel_header_text.test.tsx b/webapp/channels/src/components/channel_header/channel_header_text.test.tsx new file mode 100644 index 0000000000..4719ca194d --- /dev/null +++ b/webapp/channels/src/components/channel_header/channel_header_text.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + 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( + , + state, + ); + + expect(screen.getByText('Add a channel header')).toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/components/channel_header/channel_header_text.tsx b/webapp/channels/src/components/channel_header/channel_header_text.tsx new file mode 100644 index 0000000000..f19e668cd3 --- /dev/null +++ b/webapp/channels/src/components/channel_header/channel_header_text.tsx @@ -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 ( + + ); + } + + // 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 ; + } + + // 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 ( + + + + ); +} + +function AddChannelHeaderTextButton({channel}: {channel: Channel}) { + const dispatch = useDispatch(); + + function handleClick() { + dispatch(openModal({ + modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER, + dialogType: EditChannelHeaderModal, + dialogProps: {channel}, + })); + } + + return ( + + ); +} diff --git a/webapp/channels/src/components/channel_header/channel_header_text_popover.scss b/webapp/channels/src/components/channel_header/channel_header_text_popover.scss new file mode 100644 index 0000000000..e883d76873 --- /dev/null +++ b/webapp/channels/src/components/channel_header/channel_header_text_popover.scss @@ -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; + } + } +} diff --git a/webapp/channels/src/components/channel_header/channel_header_text_popover.tsx b/webapp/channels/src/components/channel_header/channel_header_text_popover.tsx new file mode 100644 index 0000000000..be15f3c2c9 --- /dev/null +++ b/webapp/channels/src/components/channel_header/channel_header_text_popover.tsx @@ -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(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) { + dispatch(handleFormattedTextClick(event, currentRelativeTeamUrl)); + } + + return ( + <> +
+ +
+ + {isMounted && ( + + + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+ +
+
+
+ )} + + ); +} + +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)); +} diff --git a/webapp/channels/src/components/channel_header/index.ts b/webapp/channels/src/components/channel_header/index.ts index dd37e150de..9fdace4ecf 100644 --- a/webapp/channels/src/components/channel_header/index.ts +++ b/webapp/channels/src/components/channel_header/index.ts @@ -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(connect(makeMapStateToProps, mapDispatchToProps)(ChannelHeader)); +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +export type PropsFromRedux = ConnectedProps; + +export default withRouter(connector(ChannelHeader)); diff --git a/webapp/channels/src/components/profile_popover/profile_popover.scss b/webapp/channels/src/components/profile_popover/profile_popover.scss index ae0d6e82c2..ddf5a53580 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover.scss +++ b/webapp/channels/src/components/profile_popover/profile_popover.scss @@ -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; diff --git a/webapp/channels/src/components/profile_popover/profile_popover_controller.tsx b/webapp/channels/src/components/profile_popover/profile_popover_controller.tsx index 133c32ece8..dc303e41a0 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover_controller.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover_controller.tsx @@ -118,7 +118,6 @@ export function ProfilePopoverController {isMounted && ( diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 05c5c5857f..a7852effec 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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", diff --git a/webapp/channels/src/sass/components/_popover.scss b/webapp/channels/src/sass/components/_popover.scss index 7982493251..1bc2da0d3a 100644 --- a/webapp/channels/src/sass/components/_popover.scss +++ b/webapp/channels/src/sass/components/_popover.scss @@ -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; diff --git a/webapp/channels/src/sass/layout/_headers.scss b/webapp/channels/src/sass/layout/_headers.scss index 78431ec107..e6d7e18adb 100644 --- a/webapp/channels/src/sass/layout/_headers.scss +++ b/webapp/channels/src/sass/layout/_headers.scss @@ -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; diff --git a/webapp/channels/src/sass/layout/_markdown.scss b/webapp/channels/src/sass/layout/_markdown.scss index 76cd9e82d7..93c7c072e2 100644 --- a/webapp/channels/src/sass/layout/_markdown.scss +++ b/webapp/channels/src/sass/layout/_markdown.scss @@ -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; diff --git a/webapp/channels/src/sass/responsive/_mobile.scss b/webapp/channels/src/sass/responsive/_mobile.scss index 6397185a5d..7a6a2fffcd 100644 --- a/webapp/channels/src/sass/responsive/_mobile.scss +++ b/webapp/channels/src/sass/responsive/_mobile.scss @@ -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 { diff --git a/webapp/channels/src/sass/responsive/_tablet.scss b/webapp/channels/src/sass/responsive/_tablet.scss index c7f1e5738d..a08ff8b5a9 100644 --- a/webapp/channels/src/sass/responsive/_tablet.scss +++ b/webapp/channels/src/sass/responsive/_tablet.scss @@ -119,11 +119,6 @@ width: 274px; } - .channel-header__popover { - top: 44px !important; - left: 236px !important; - } - .col-sm-auto { float: left; } diff --git a/webapp/channels/src/sass/utils/_variables.scss b/webapp/channels/src/sass/utils/_variables.scss index ac823ea3bc..922e615b86 100644 --- a/webapp/channels/src/sass/utils/_variables.scss +++ b/webapp/channels/src/sass/utils/_variables.scss @@ -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;