diff --git a/e2e/cypress/tests/integration/channels/channel/archived_channels_1_spec.js b/e2e/cypress/tests/integration/channels/channel/archived_channels_1_spec.js index f6f6a3e186..1880dafb8a 100644 --- a/e2e/cypress/tests/integration/channels/channel/archived_channels_1_spec.js +++ b/e2e/cypress/tests/integration/channels/channel/archived_channels_1_spec.js @@ -97,7 +97,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('#moreChannelsModal').should('be.visible').within(() => { // # Click on dropdown - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText('Show: Public Channels').should('be.visible').click(); // # Click archived channels cy.findByText('Archived Channels').click(); @@ -143,9 +143,9 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('#moreChannelsModal').should('be.visible').within(() => { + cy.get('.more-modal').should('be.visible').within(() => { // # Public channel list opens by default - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText('Show: Public Channels').should('be.visible').click(); // # Click on archived channels cy.findByText('Archived Channels').click(); @@ -196,9 +196,9 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('#moreChannelsModal').should('be.visible').within(() => { + cy.get('.more-modal').should('be.visible').within(() => { // # Public channels are shown by default - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText('Show: Public Channels').should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -250,9 +250,9 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('#moreChannelsModal').should('be.visible').within(() => { + cy.get('.more-modal').should('be.visible').within(() => { // # Show public channels is visible by default - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText('Show: Public Channels').should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -286,7 +286,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens and lands on public channels cy.get('#moreChannelsModal').should('be.visible').within(() => { - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText('Show: Public Channels').should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); diff --git a/e2e/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e/cypress/tests/integration/channels/channel/more_channels_spec.js index 7848ae787c..d83797db07 100644 --- a/e2e/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -65,7 +65,7 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').wait(TIMEOUTS.HALF_SEC); + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { @@ -80,8 +80,8 @@ describe('Channels', () => { }); }); - // # Verify that the modal is not closed - cy.get('#moreChannelsModal').should('exist'); + // # Verify that the modal is closed and it's redirected to the selected channel + cy.get('#moreChannelsModal').should('not.exist'); cy.url().should('include', `/${testTeam.name}/channels/${testChannel.name}`); // # Login as channel admin and go directly to the channel @@ -113,7 +113,7 @@ describe('Channels', () => { cy.findByText('Archived Channels').should('be.visible').click(); // * Channel test should be visible as an archived channel in the list - cy.wrap(el).should('contain', 'Channel Type: Archived'); + cy.wrap(el).should('contain', 'Show: Archived Channels'); }); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); @@ -196,7 +196,7 @@ describe('Channels', () => { // * Dropdown should be visible, defaulting to "Public Channels" cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { - cy.wrap(el).should('contain', 'Channel Type: Public'); + cy.wrap(el).should('contain', 'Show: Public Channels'); }); // * Users should be able to type and search @@ -207,12 +207,12 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Users should be able to switch to "Archived Channels" list - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').click().within((el) => { + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').click().within((el) => { // # Click on archived channels item cy.findByText('Archived Channels').should('be.visible').click(); // * Modal should show the archived channels list - cy.wrap(el).should('contain', 'Channel Type: Archived'); + cy.wrap(el).should('contain', 'Show: Archived Channels'); }).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').clear(); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); @@ -250,7 +250,7 @@ function verifyMoreChannelsModal(isEnabled) { // * Verify that the more channels modal is open and with or without option to view archived channels cy.get('#moreChannelsModal').should('be.visible').within(() => { if (isEnabled) { - cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Channel Type: Public'); + cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Show: Public Channels'); } else { cy.get('#channelsMoreDropdown').should('not.exist'); } diff --git a/e2e/cypress/tests/integration/channels/channel/more_public_channels_spec.js b/e2e/cypress/tests/integration/channels/channel/more_public_channels_spec.js index 0d38de9467..5808654971 100644 --- a/e2e/cypress/tests/integration/channels/channel/more_public_channels_spec.js +++ b/e2e/cypress/tests/integration/channels/channel/more_public_channels_spec.js @@ -11,7 +11,8 @@ // Group: @channels @channel function verifyNoChannelToJoinMessage(isVisible) { - cy.findByText('No public channels').should(isVisible ? 'be.visible' : 'not.exist'); + cy.findByText('No more channels to join').should(isVisible ? 'be.visible' : 'not.exist'); + cy.findByText('Click \'Create New Channel\' to make a new one').should(isVisible ? 'be.visible' : 'not.exist'); } describe('more public channels', () => { @@ -52,10 +53,7 @@ describe('more public channels', () => { cy.uiBrowseOrCreateChannel('Browse Channels').click(); // * Assert that the moreChannelsModel is visible - cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => { - // # Click hide joined checkbox - cy.findByText('Hide Joined').should('be.visible').click(); - + cy.findByRole('dialog', {name: 'More Channels'}).should('be.visible').within(() => { // * Assert that the moreChannelsList is visible and the number of channels is 31 cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 31); @@ -88,9 +86,9 @@ describe('more public channels', () => { cy.uiBrowseOrCreateChannel('Browse Channels').click(); // * Assert the moreChannelsModel is visible - cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => { - // # Click hide joined checkbox - cy.findByText('Hide Joined').should('be.visible').click(); + cy.findByRole('dialog', {name: 'More Channels'}).should('be.visible').within(() => { + // * Assert the moreChannelsList does have one child + cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1); // * Assert that the "No more channels to join" message is visible verifyNoChannelToJoinMessage(true); diff --git a/e2e/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts b/e2e/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts index b94d16acfa..17114e21aa 100644 --- a/e2e/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts +++ b/e2e/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts @@ -68,13 +68,13 @@ describe('Channel sidebar', () => { cy.get('.AddChannelDropdown .MenuItem:contains(Browse Channels) button').should('be.visible').click(); // * Verify that the more channels modal is visible - cy.get('#moreChannelsModal').should('be.visible'); + cy.get('.more-modal').should('be.visible'); // Click the Off-Topic channel - cy.findByText('Off-Topic').should('be.visible').click(); + cy.get('.more-modal button:contains(Off-Topic)').should('be.visible').click(); // Verify that new channel is in the sidebar and is active - cy.get('#moreChannelsModal').should('exist'); + cy.get('.more-modal').should('not.exist'); cy.url().should('include', `/${teamName}/channels/off-topic`); cy.get('#channelHeaderTitle').should('contain', 'Off-Topic'); cy.get('.SidebarChannel.active:contains(Off-Topic)').should('be.visible'); diff --git a/e2e/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js b/e2e/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js index adf5478b55..a068d15f9f 100644 --- a/e2e/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js +++ b/e2e/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js @@ -104,26 +104,30 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => { cy.uiBrowseOrCreateChannel('Browse Channels').click(); // * Verify the accessibility support in More Channels Dialog - cy.findByRole('dialog', {name: 'Browse Channels'}).within(() => { - cy.findByRole('heading', {name: 'Browse Channels'}); + cy.findByRole('dialog', {name: 'More Channels'}).within(() => { + cy.findByRole('heading', {name: 'More Channels'}); // * Verify the accessibility support in search input cy.findByPlaceholderText('Search channels'); - cy.get('#moreChannelsList').should('be.visible').then((el) => { + cy.waitUntil(() => cy.get('#moreChannelsList').then((el) => { return el[0].children.length === 2; - }); + })); - // # Hide already joined channels - cy.findByText('Hide Joined').click(); - - // # Focus on the Create Channel button and TAB three time - cy.get('#createNewChannelButton').focus().tab().tab().tab(); + // # Focus on the Create Channel button and TAB twice + cy.get('#createNewChannel').focus().tab().tab(); // * Verify channel name is highlighted and reader reads the channel name and channel description - cy.get('#moreChannelsList').within(() => { + cy.get('#moreChannelsList').children().eq(0).within(() => { const selectedChannel = getChannelAriaLabel(channel); - cy.findByLabelText(selectedChannel).should('be.visible').should('be.focused'); + cy.findByLabelText(selectedChannel).should('be.focused'); + + // * Press Tab and verify if focus changes to Join button + cy.focused().tab(); + cy.findByText('Join').parent().should('be.focused'); + + // * Verify previous button should no longer be focused + cy.findByLabelText(selectedChannel).should('not.be.focused'); }); // * Press Tab again and verify if focus changes to next row diff --git a/e2e/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js b/e2e/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js index 75264a2aea..c7660753e0 100644 --- a/e2e/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js +++ b/e2e/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js @@ -234,7 +234,7 @@ context('ldap', () => { // * Search private channel name and make sure it isn't there in public channel directory cy.get('#searchChannelsTextbox').type(testChannel.display_name); - cy.get('#moreChannelsList').should('include.text', 'No results for'); + cy.get('#moreChannelsList').should('include.text', 'No more channels to join'); }); it('MM-T2629 - Private to public - More....', () => { @@ -473,7 +473,7 @@ context('ldap', () => { // * Search private channel name and make sure it isn't there in public channel directory cy.get('#searchChannelsTextbox').type(publicChannel.display_name); - cy.get('#moreChannelsList').should('include.text', 'No results for'); + cy.get('#moreChannelsList').should('include.text', 'No more channels to join'); }); }); diff --git a/webapp/channels/src/actions/channel_actions.test.ts b/webapp/channels/src/actions/channel_actions.test.ts index aac40512ec..6b247396b2 100644 --- a/webapp/channels/src/actions/channel_actions.test.ts +++ b/webapp/channels/src/actions/channel_actions.test.ts @@ -167,7 +167,7 @@ describe('Actions.Channel', () => { }], }]; - await testStore.dispatch(searchMoreChannels('', false, true)); + await testStore.dispatch(searchMoreChannels('', false)); expect(testStore.getActions()).toEqual(expectedActions); }); diff --git a/webapp/channels/src/actions/channel_actions.ts b/webapp/channels/src/actions/channel_actions.ts index 0add017514..b3c9ffb156 100644 --- a/webapp/channels/src/actions/channel_actions.ts +++ b/webapp/channels/src/actions/channel_actions.ts @@ -109,7 +109,7 @@ export function loadChannelsForCurrentUser(): ActionFunc { }; } -export function searchMoreChannels(term: string, showArchivedChannels: boolean, hideJoinedChannels: boolean): ActionFunc { +export function searchMoreChannels(term: string, showArchivedChannels: boolean): ActionFunc { return async (dispatch, getState) => { const state = getState(); const teamId = getCurrentTeamId(state); @@ -121,7 +121,9 @@ export function searchMoreChannels(term: string, showArchivedChannels: boolean, const {data, error} = await dispatch(ChannelActions.searchChannels(teamId, term, showArchivedChannels)); if (data) { const myMembers = getMyChannelMemberships(state); - const channels = hideJoinedChannels ? (data as Channel[]).filter((channel) => !myMembers[channel.id]) : data; + + // When searching public channels, only get channels user is not a member of + const channels = showArchivedChannels ? data : (data as Channel[]).filter((c) => !myMembers[c.id]); return {data: channels}; } diff --git a/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.jsx.snap b/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.jsx.snap new file mode 100644 index 0000000000..eff77678a4 --- /dev/null +++ b/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.jsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/SearchableChannelList should match init snapshot 1`] = ` +
+
+
+ +
+
+
+
+ +
+
+
+
+`; diff --git a/webapp/channels/src/components/common/svg_images_components/magnifying_glass_svg.tsx b/webapp/channels/src/components/common/svg_images_components/magnifying_glass_svg.tsx deleted file mode 100644 index 75b2ec8d3d..0000000000 --- a/webapp/channels/src/components/common/svg_images_components/magnifying_glass_svg.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {SVGProps} from 'react'; - -const SvgComponent = (props: SVGProps) => ( - - - - - - - -); - -export default SvgComponent; diff --git a/webapp/channels/src/components/generic_modal.scss b/webapp/channels/src/components/generic_modal.scss index 32ce008f2a..6cbca8629c 100644 --- a/webapp/channels/src/components/generic_modal.scss +++ b/webapp/channels/src/components/generic_modal.scss @@ -133,7 +133,6 @@ } .GenericModal__header { - width: 85%; padding: 0; border-top-left-radius: 12px; border-top-right-radius: 12px; diff --git a/webapp/channels/src/components/more_channels/__snapshots__/more_channels.test.tsx.snap b/webapp/channels/src/components/more_channels/__snapshots__/more_channels.test.tsx.snap index 1f2cd1c3d0..fe1acbf101 100644 --- a/webapp/channels/src/components/more_channels/__snapshots__/more_channels.test.tsx.snap +++ b/webapp/channels/src/components/more_channels/__snapshots__/more_channels.test.tsx.snap @@ -1,15 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/MoreChannels should match snapshot and state 1`] = ` - + + + + + - - } - id="moreChannelsModal" - keyboardEscape={true} - modalHeaderText={ - + + + +

+ +

+ + } + search={[Function]} + shouldShowArchivedChannels={false} + toggleArchivedChannels={[Function]} /> - } - onExited={[Function]} - show={true} -> - -
+ + `; diff --git a/webapp/channels/src/components/more_channels/__snapshots__/searchable_channel_list.test.jsx.snap b/webapp/channels/src/components/more_channels/__snapshots__/searchable_channel_list.test.jsx.snap deleted file mode 100644 index 1118d16bf2..0000000000 --- a/webapp/channels/src/components/more_channels/__snapshots__/searchable_channel_list.test.jsx.snap +++ /dev/null @@ -1,82 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/SearchableChannelList should match init snapshot 1`] = ` -
-
- - -
-
- - 0 Results - -
-
-
-
-
-
-
- -
-
-
-
-`; diff --git a/webapp/channels/src/components/more_channels/index.ts b/webapp/channels/src/components/more_channels/index.ts index 7afbea20e2..d21425d99e 100644 --- a/webapp/channels/src/components/more_channels/index.ts +++ b/webapp/channels/src/components/more_channels/index.ts @@ -12,29 +12,24 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {Action, ActionResult} from 'mattermost-redux/types/actions'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getChannels, getArchivedChannels, joinChannel, getChannelStats} from 'mattermost-redux/actions/channels'; -import {getChannelsInCurrentTeam, getMyChannelMemberships, getAllChannelStats} from 'mattermost-redux/selectors/entities/channels'; - -import {Constants, StoragePrefixes} from 'utils/constants'; +import {getChannels, getArchivedChannels, joinChannel} from 'mattermost-redux/actions/channels'; +import {getOtherChannels, getChannelsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels'; import {searchMoreChannels} from 'actions/channel_actions'; import {openModal, closeModal} from 'actions/views/modals'; -import {setGlobalItem} from 'actions/storage'; import {closeRightHandSide} from 'actions/views/rhs'; import {getIsRhsOpen, getRhsState} from 'selectors/rhs'; -import {GlobalState} from 'types/store'; import {ModalData} from 'types/actions'; - -import {makeGetGlobalItem} from 'selectors/storage'; +import {GlobalState} from 'types/store'; import MoreChannels from './more_channels'; -const getChannelsWithoutArchived = createSelector( - 'getChannelsWithoutArchived', - getChannelsInCurrentTeam, - (channels: Channel[]) => channels && channels.filter((c) => c.delete_at === 0 && c.type !== Constants.PRIVATE_CHANNEL), +const getNotArchivedOtherChannels = createSelector( + 'getNotArchivedOtherChannels', + getOtherChannels, + (channels: Channel[]) => channels && channels.filter((c) => c.delete_at === 0), ); const getArchivedOtherChannels = createSelector( @@ -45,19 +40,15 @@ const getArchivedOtherChannels = createSelector( function mapStateToProps(state: GlobalState) { const team = getCurrentTeam(state) || {}; - const getGlobalItem = makeGetGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, 'false'); return { - channels: getChannelsWithoutArchived(state) || [], + channels: getNotArchivedOtherChannels(state) || [], archivedChannels: getArchivedOtherChannels(state) || [], currentUserId: getCurrentUserId(state), teamId: team.id, teamName: team.name, channelsRequestStarted: state.requests.channels.getChannels.status === RequestStatus.STARTED, canShowArchivedChannels: (getConfig(state).ExperimentalViewArchivedChannels === 'true'), - myChannelMemberships: getMyChannelMemberships(state) || {}, - allChannelStats: getAllChannelStats(state) || {}, - shouldHideJoinedChannels: getGlobalItem(state) === 'true', rhsState: getRhsState(state), rhsOpen: getIsRhsOpen(state), }; @@ -70,8 +61,6 @@ type Actions = { searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean) => Promise; openModal:

(modalData: ModalData

) => void; closeModal: (modalId: string) => void; - getChannelStats: (channelId: string) => void; - setGlobalItem: (name: string, value: string) => void; closeRightHandSide: () => void; } @@ -84,8 +73,6 @@ function mapDispatchToProps(dispatch: Dispatch) { searchMoreChannels, openModal, closeModal, - getChannelStats, - setGlobalItem, closeRightHandSide, }, dispatch), }; diff --git a/webapp/channels/src/components/more_channels/more_channels.scss b/webapp/channels/src/components/more_channels/more_channels.scss deleted file mode 100644 index d86fd138a7..0000000000 --- a/webapp/channels/src/components/more_channels/more_channels.scss +++ /dev/null @@ -1,295 +0,0 @@ -@charset 'UTF-8'; - -#moreChannelsModal { - .modal-content { - min-height: 600px; - max-height: calc(50vh - 240px); - } - - .modal-dialog { - margin-top: calc(45vh - 240px) !important; - } - - .filter-row--full { - position: relative; - margin: 0 32px; - - .input-clear { - top: 16px; - right: 16px; - } - - #searchIcon { - position: absolute; - top: 14px; - left: 16px; - color: rgba(var(--center-channel-color-rgb), 0.64); - pointer-events: none; - } - - #searchChannelsTextbox { - height: 48px; - padding-left: 40px; - border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); - box-shadow: none; - font-size: 16px; - - &::placeholder { - color: var(--center-channel-color); - } - - &:focus { - border: 2px solid var(--button-bg); - } - } - } - - .more-modal__dropdown { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 32px; - border-bottom: solid 1px rgba(var(--center-channel-color-rgb), 0.16); - margin: 0; - - span { - color: rgba(var(--center-channel-color-rgb), 0.64); - font-size: 12px; - line-height: 16px; - } - - .MenuItem__primary-text { - width: 100%; - color: var(--center-channel-color); - font-size: 14px; - font-weight: 400; - line-height: 20px; - - svg { - margin-left: auto; - } - } - - .Menu__content { - border-color: rgba(var(--center-channel-color-rgb), 0.16); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); - } - - #channelCountLabel { - color: var(--center-channel-color); - font-size: 12px; - font-weight: 400; - } - - #modalPreferenceContainer { - display: flex; - align-items: center; - justify-content: center; - - .get-app__checkbox { - display: flex; - width: 16px; - height: 16px; - align-items: center; - border: 1px solid rgba(var(--center-channel-color-rgb), 0.24); - } - - #hideJoinedPreferenceCheckbox { - display: flex; - align-items: center; - cursor: pointer; - } - - #channelsMoreDropdown { - margin: 0 8px; - } - - #menuWrapper { - display: flex; - align-items: center; - justify-content: center; - padding: 4px 6px 4px 8px; - } - - .MenuWrapper:hover { - background-color: rgba(var(--center-channel-color-rgb), 0.08); - border-radius: 4px; - } - - .MenuWrapper--open { - background-color: rgba(var(--button-bg-rgb), 0.12); - border-radius: 4px; - - &:hover { - background-color: rgba(var(--button-bg-rgb), 0.12); - } - } - } - } - - .modal-body { - padding: 15px 0 0; - - .filtered-user-list { - height: 500px; - } - - .more-modal__row { - padding: 0 32px; - border-bottom: none; - - .more-modal__details { - padding-left: 0; - color: rgba(var(--center-channel-color-rgb), 0.56); - - svg { - flex-shrink: 0; - } - - .more-modal__name { - align-items: center; - margin-top: 0; - - span { - color: var(--center-channel-color); - font-weight: 500; - } - } - - #channelPurposeContainer { - display: flex; - align-items: center; - justify-content: flex-start; - - .dot { - width: 3px; - height: 3px; - flex-shrink: 0; - background-color: rgba(var(--center-channel-color-rgb), 0.56); - border-radius: 50%; - } - - .more-modal__description { - margin-left: 4px; - font-weight: 400; - } - - #membershipIndicatorContainer { - display: flex; - align-items: center; - - span, - svg { - color: var(--online-indicator); - } - } - - span { - margin: 0 4px; - font-size: 12px; - font-weight: 600; - line-height: 12px; - opacity: 1; - } - } - } - - .more-modal__actions { - button { - display: none; - min-width: 54px; - height: 32px; - font-size: 12px; - font-weight: 600; - } - } - } - - .more-modal__row:hover, - .more-modal__row:focus { - background-color: rgba(var(--center-channel-color-rgb), 0.08); - cursor: pointer; - - .more-modal__actions { - .primaryButton, - .outlineButton { - display: inline-block; - } - } - } - - .form-group { - padding: 0 32px; - margin-bottom: 0; - } - - ::-webkit-scrollbar { - width: 4px; - } - - ::-webkit-scrollbar-track { - background: none; - } - } - - .modal-header { - .GenericModal__header { - display: flex; - width: 95%; - align-items: center; - justify-content: space-between; - padding-right: 4px; - } - - .close { - top: 22px; - } - } - - .outlineButton { - border: 1px solid var(--button-bg); - background: none; - border-radius: 4px; - color: var(--button-bg); - font-size: 12px; - font-weight: 600; - line-height: 16px; - } - - .outlineButton:hover { - background-color: rgba(var(--button-bg-rgb), 0.08); - } - - .filter-controls { - padding: 0; - - button { - min-width: 72px; - margin: 8px 32px; - } - } -} - -#moreChannelsList { - .primary-message { - margin-top: 8px; - color: var(--center-channel-color); - line-height: 28px; - } - - .secondary-message { - margin-bottom: 30px; - } - - .primaryButton { - background-color: var(--button-bg); - border-radius: 4px; - color: var(--button-color); - font-size: 14px; - font-weight: 600; - } - - #createNewChannelButton { - padding: 10px 20px; - } -} diff --git a/webapp/channels/src/components/more_channels/more_channels.test.tsx b/webapp/channels/src/components/more_channels/more_channels.test.tsx index 4d05021650..a591bfef5b 100644 --- a/webapp/channels/src/components/more_channels/more_channels.test.tsx +++ b/webapp/channels/src/components/more_channels/more_channels.test.tsx @@ -7,7 +7,7 @@ import {shallow} from 'enzyme'; import {ActionResult} from 'mattermost-redux/types/actions'; import MoreChannels, {Props} from 'components/more_channels/more_channels'; -import SearchableChannelList from 'components/more_channels/searchable_channel_list.jsx'; +import SearchableChannelList from 'components/searchable_channel_list.jsx'; import {getHistory} from 'utils/browser_history'; import {TestHelper} from 'utils/test_helper'; @@ -59,16 +59,7 @@ describe('components/MoreChannels', () => { }; const baseProps: Props = { - channels: [ - TestHelper.getChannelMock({ - id: 'channel-1', - name: 'channel-1', - }), - TestHelper.getChannelMock({ - id: 'channel-2', - name: 'channel-2', - }), - ], + channels: [TestHelper.getChannelMock({})], archivedChannels: [TestHelper.getChannelMock({ id: 'channel_id_2', team_id: 'channel_team_2', @@ -82,14 +73,6 @@ describe('components/MoreChannels', () => { teamName: 'team_name', channelsRequestStarted: false, canShowArchivedChannels: true, - myChannelMemberships: { - 'channel-2': TestHelper.getChannelMembershipMock({ - channel_id: 'channel-2', - user_id: 'user-1', - }), - }, - allChannelStats: {}, - shouldHideJoinedChannels: false, actions: { getChannels: jest.fn(), getArchivedChannels: jest.fn(), @@ -97,8 +80,6 @@ describe('components/MoreChannels', () => { searchMoreChannels: jest.fn(channelActions.searchMoreChannels), openModal: jest.fn(), closeModal: jest.fn(), - getChannelStats: jest.fn(), - setGlobalItem: jest.fn(), closeRightHandSide: jest.fn(), }, }; @@ -110,6 +91,7 @@ describe('components/MoreChannels', () => { expect(wrapper).toMatchSnapshot(); expect(wrapper.state('searchedChannels')).toEqual([]); + expect(wrapper.state('show')).toEqual(true); expect(wrapper.state('shouldShowArchivedChannels')).toEqual(false); expect(wrapper.state('search')).toEqual(false); expect(wrapper.state('serverError')).toBeNull(); @@ -120,6 +102,16 @@ describe('components/MoreChannels', () => { expect(wrapper.instance().props.actions.getChannels).toHaveBeenCalledWith(wrapper.instance().props.teamId, 0, 100); }); + test('should match state on handleHide', () => { + const wrapper = shallow( + , + ); + wrapper.setState({show: true}); + + wrapper.instance().handleHide(); + expect(wrapper.state('show')).toEqual(false); + }); + test('should call closeModal on handleExit', () => { const wrapper = shallow( , @@ -160,7 +152,7 @@ describe('components/MoreChannels', () => { , ); - wrapper.setState({loading: false, search: true, searching: true}); + wrapper.setState({search: true, searching: true}); const searchList = wrapper.find(SearchableChannelList); expect(searchList.props().loading).toEqual(true); }); @@ -219,6 +211,7 @@ describe('components/MoreChannels', () => { process.nextTick(() => { expect(getHistory().push).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledTimes(1); + expect(wrapper.state('show')).toEqual(false); done(); }); }); @@ -256,7 +249,7 @@ describe('components/MoreChannels', () => { jest.runOnlyPendingTimers(); expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('fail', false, false); + expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('fail', false); process.nextTick(() => { expect(wrapper.state('search')).toEqual(true); expect(wrapper.state('searching')).toEqual(false); @@ -283,7 +276,7 @@ describe('components/MoreChannels', () => { jest.runOnlyPendingTimers(); expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', false, false); + expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', false); process.nextTick(() => { expect(wrapper.state('search')).toEqual(true); expect(wrapper.state('searching')).toEqual(false); @@ -310,7 +303,7 @@ describe('components/MoreChannels', () => { jest.runOnlyPendingTimers(); expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', true, false); + expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', true); process.nextTick(() => { expect(wrapper.state('search')).toEqual(true); expect(wrapper.state('searching')).toEqual(false); @@ -318,16 +311,4 @@ describe('components/MoreChannels', () => { done(); }); }); - - test('should hide joined channels from channels props when shouldHideJoinedChannels prop is true', () => { - const props = { - ...baseProps, - shouldHideJoinedChannels: true, - }; - const wrapper = shallow( - , - ); - - expect(wrapper.instance().activeChannels).not.toContain(baseProps.channels[1]); - }); }); diff --git a/webapp/channels/src/components/more_channels/more_channels.tsx b/webapp/channels/src/components/more_channels/more_channels.tsx index 235744166e..b51606db19 100644 --- a/webapp/channels/src/components/more_channels/more_channels.tsx +++ b/webapp/channels/src/components/more_channels/more_channels.tsx @@ -2,32 +2,23 @@ // See LICENSE.txt for license information. import React from 'react'; +import {Modal} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; -import classNames from 'classnames'; - import {ActionResult} from 'mattermost-redux/types/actions'; -import {Channel, ChannelMembership, ChannelStats} from '@mattermost/types/channels'; +import {Channel} from '@mattermost/types/channels'; import Permissions from 'mattermost-redux/constants/permissions'; -import {RelationOneToOne} from '@mattermost/types/utilities'; - import NewChannelModal from 'components/new_channel_modal/new_channel_modal'; +import SearchableChannelList from 'components/searchable_channel_list.jsx'; import TeamPermissionGate from 'components/permissions_gates/team_permission_gate'; -import GenericModal from 'components/generic_modal'; -import LoadingScreen from 'components/loading_screen'; import {ModalData} from 'types/actions'; import {RhsState} from 'types/store/rhs'; import {getHistory} from 'utils/browser_history'; -import {ModalIdentifiers, StoragePrefixes, RHSStates} from 'utils/constants'; +import {ModalIdentifiers, RHSStates} from 'utils/constants'; import {getRelativeChannelURL} from 'utils/url'; -import {localizeMessage} from 'utils/utils'; - -import SearchableChannelList from './searchable_channel_list'; - -import './more_channels.scss'; const CHANNELS_CHUNK_SIZE = 50; const CHANNELS_PER_PAGE = 50; @@ -37,15 +28,9 @@ type Actions = { getChannels: (teamId: string, page: number, perPage: number) => void; getArchivedChannels: (teamId: string, page: number, channelsPerPage: number) => void; joinChannel: (currentUserId: string, teamId: string, channelId: string) => Promise; - searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean, shouldHideJoinedChannels: boolean) => Promise; + searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean) => Promise; openModal:

(modalData: ModalData

) => void; closeModal: (modalId: string) => void; - getChannelStats: (channelId: string) => void; - - /* - * Function to set a key-value pair in the local storage - */ - setGlobalItem: (name: string, value: string) => void; closeRightHandSide: () => void; } @@ -58,27 +43,23 @@ export type Props = { channelsRequestStarted?: boolean; canShowArchivedChannels?: boolean; morePublicChannelsModalType?: string; - myChannelMemberships: RelationOneToOne; - allChannelStats: RelationOneToOne; - shouldHideJoinedChannels: boolean; rhsState?: RhsState; rhsOpen?: boolean; actions: Actions; } type State = { + show: boolean; shouldShowArchivedChannels: boolean; search: boolean; searchedChannels: Channel[]; serverError: React.ReactNode | string; searching: boolean; searchTerm: string; - loading: boolean; } export default class MoreChannels extends React.PureComponent { public searchTimeoutId: number; - activeChannels: Channel[] = []; constructor(props: Props) { super(props); @@ -86,27 +67,25 @@ export default class MoreChannels extends React.PureComponent { this.searchTimeoutId = 0; this.state = { + show: true, shouldShowArchivedChannels: this.props.morePublicChannelsModalType === 'private', search: false, searchedChannels: [], serverError: null, searching: false, searchTerm: '', - loading: true, }; } - async componentDidMount() { - await this.props.actions.getChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2); + componentDidMount() { + this.props.actions.getChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2); if (this.props.canShowArchivedChannels) { - await this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2); + this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2); } - await this.props.channels.forEach((channel) => this.props.actions.getChannelStats(channel.id)); - this.loadComplete(); } - loadComplete = () => { - this.setState({loading: false}); + handleHide = () => { + this.setState({show: false}); } handleNewChannel = () => { @@ -145,17 +124,14 @@ export default class MoreChannels extends React.PureComponent { handleJoin = async (channel: Channel, done: () => void) => { const {actions, currentUserId, teamId, teamName} = this.props; - let result; + const result = await actions.joinChannel(currentUserId, teamId, channel.id); - if (!this.isMemberOfChannel(channel.id)) { - result = await actions.joinChannel(currentUserId, teamId, channel.id); - } - - if (result?.error) { + if (result.error) { this.setState({serverError: result.error.message}); } else { getHistory().push(getRelativeChannelURL(teamName, channel.name)); this.closeEditRHS(); + this.handleHide(); } if (done) { @@ -177,7 +153,7 @@ export default class MoreChannels extends React.PureComponent { const searchTimeoutId = window.setTimeout( async () => { try { - const {data} = await this.props.actions.searchMoreChannels(term, this.state.shouldShowArchivedChannels, this.props.shouldHideJoinedChannels); + const {data} = await this.props.actions.searchMoreChannels(term, this.state.shouldShowArchivedChannels); if (searchTimeoutId !== this.searchTimeoutId) { return; } @@ -207,47 +183,29 @@ export default class MoreChannels extends React.PureComponent { this.setState({shouldShowArchivedChannels}); } - isMemberOfChannel(channelId: string) { - return this.props.myChannelMemberships[channelId]; - } - - handleShowJoinedChannelsPreference = (shouldHideJoinedChannels: boolean) => { - // search again when switching channels to update search results - this.search(this.state.searchTerm); - this.props.actions.setGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, shouldHideJoinedChannels.toString()); - } - - otherChannelsWithoutJoined = this.props.channels.filter((channel) => !this.isMemberOfChannel(channel.id)); - archivedChannelsWithoutJoined = this.props.archivedChannels.filter((channel) => !this.isMemberOfChannel(channel.id)); - render() { const { channels, archivedChannels, teamId, channelsRequestStarted, - shouldHideJoinedChannels, } = this.props; const { search, searchedChannels, serverError: serverErrorState, + show, searching, shouldShowArchivedChannels, } = this.state; - const otherChannelsWithoutJoined = channels.filter((channel) => !this.isMemberOfChannel(channel.id)); - const archivedChannelsWithoutJoined = archivedChannels.filter((channel) => !this.isMemberOfChannel(channel.id)); + let activeChannels; - if (shouldShowArchivedChannels && shouldHideJoinedChannels) { - this.activeChannels = search ? searchedChannels : archivedChannelsWithoutJoined; - } else if (shouldShowArchivedChannels && !shouldHideJoinedChannels) { - this.activeChannels = search ? searchedChannels : archivedChannels; - } else if (!shouldShowArchivedChannels && shouldHideJoinedChannels) { - this.activeChannels = search ? searchedChannels : otherChannelsWithoutJoined; + if (shouldShowArchivedChannels) { + activeChannels = search ? searchedChannels : archivedChannels; } else { - this.activeChannels = search ? searchedChannels : channels; + activeChannels = search ? searchedChannels : channels; } let serverError; @@ -256,87 +214,87 @@ export default class MoreChannels extends React.PureComponent {

; } - const createNewChannelButton = (className: string, icon?: JSX.Element) => { - const buttonClassName = classNames('btn', className); - return ( - + - - ); - }; - - const noResultsText = ( - <> -

-

- {createNewChannelButton('primaryButton', )} - + + ); - const body = this.state.loading ? : ( + const createChannelHelpText = ( + +

+ +

+
+ ); + + const body = ( {serverError} ); - const title = ( - - ); - return ( - - {body} - + + + + + {createNewChannelButton} + + + {body} + + ); } } diff --git a/webapp/channels/src/components/more_channels/searchable_channel_list.jsx b/webapp/channels/src/components/more_channels/searchable_channel_list.jsx deleted file mode 100644 index 0efe317296..0000000000 --- a/webapp/channels/src/components/more_channels/searchable_channel_list.jsx +++ /dev/null @@ -1,483 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import {AccountOutlineIcon, ArchiveOutlineIcon, CheckIcon, ChevronDownIcon, GlobeIcon, LockOutlineIcon, MagnifyIcon} from '@mattermost/compass-icons/components'; - -import classNames from 'classnames'; - -import {isPrivateChannel} from 'mattermost-redux/utils/channel_utils'; - -import LoadingScreen from 'components/loading_screen'; -import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; -import QuickInput from 'components/quick_input'; -import LocalizedInput from 'components/localized_input/localized_input'; -import CheckboxCheckedIcon from 'components/widgets/icons/checkbox_checked_icon'; -import MagnifyingGlassSVG from 'components/common/svg_images_components/magnifying_glass_svg'; -import MenuWrapper from 'components/widgets/menu/menu_wrapper'; -import Menu from 'components/widgets/menu/menu'; - -import {t} from 'utils/i18n'; -import * as UserAgent from 'utils/user_agent'; -import Constants, {ModalIdentifiers} from 'utils/constants'; -import {isKeyPressed, localizeMessage, localizeAndFormatMessage} from 'utils/utils'; - -import {isArchivedChannel} from 'utils/channel_utils'; - -const NEXT_BUTTON_TIMEOUT_MILLISECONDS = 500; - -export default class SearchableChannelList extends React.PureComponent { - static getDerivedStateFromProps(props, state) { - return {isSearch: props.isSearch, page: props.isSearch && !state.isSearch ? 0 : state.page}; - } - - constructor(props) { - super(props); - - this.nextTimeoutId = 0; - - this.state = { - joiningChannel: '', - page: 0, - nextDisabled: false, - channelSearchValue: '', - }; - - this.filter = React.createRef(); - this.channelListScroll = React.createRef(); - } - - componentDidMount() { - // only focus the search box on desktop so that we don't cause the keyboard to open on mobile - if (!UserAgent.isMobile() && this.filter.current) { - this.filter.current.focus(); - } - document.addEventListener('keydown', this.onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.onKeyDown); - } - - onKeyDown = (e) => { - const target = e.target; - const isEnterKeyPressed = isKeyPressed(e, Constants.KeyCodes.ENTER); - if (isEnterKeyPressed && (e.shiftKey || e.ctrlKey || e.altKey)) { - return; - } - if (isEnterKeyPressed && target.classList.contains('more-modal__row')) { - target.click(); - } - } - - handleJoin = (channel, e) => { - e.stopPropagation(); - this.setState({joiningChannel: channel.id}); - this.props.handleJoin( - channel, - () => { - this.setState({joiningChannel: ''}); - }, - ); - if (this.isMemberOfChannel(channel.id)) { - this.props.closeModal(ModalIdentifiers.MORE_CHANNELS); - } - } - - isMemberOfChannel(channelId) { - return this.props.myChannelMemberships[channelId]; - } - - createChannelRow = (channel) => { - const ariaLabel = `${channel.display_name}, ${channel.purpose}`.toLowerCase(); - let channelTypeIcon; - - let memberCount = 0; - if (this.props.allChannelStats[channel.id]) { - memberCount = this.props.allChannelStats[channel.id].member_count; - } - - if (isArchivedChannel(channel)) { - channelTypeIcon = ; - } else if (isPrivateChannel(channel)) { - channelTypeIcon = ; - } else { - channelTypeIcon = ; - } - - const membershipIndicator = this.isMemberOfChannel(channel.id) ? ( -
- - - -
- ) : null; - - const channelPurposeContainerAriaLabel = localizeAndFormatMessage( - t('more_channels.channel_purpose'), - 'Channel Information: Membership Indicator: Joined, Member count {memberCount} , Purpose: {channelPurpose}', - {memberCount, channelPurpose: channel.purpose || ''}, - ); - - const channelPurposeContainer = ( -
- {membershipIndicator} - - {memberCount} - {channel.purpose.length > 0 && } - {channel.purpose} -
- ); - - const joinViewChannelButtonClass = classNames('btn', { - outlineButton: this.isMemberOfChannel(channel.id), - primaryButton: !this.isMemberOfChannel(channel.id), - }); - - const joinViewChannelButton = ( - - ); - - return ( -
this.handleJoin(channel, e)} - tabIndex={0} - > -
-
- {channelTypeIcon} - {channel.display_name} -
- {channelPurposeContainer} -
-
- {joinViewChannelButton} -
-
- ); - } - - nextPage = (e) => { - e.preventDefault(); - this.setState({page: this.state.page + 1, nextDisabled: true}); - this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT_MILLISECONDS); - this.props.nextPage(this.state.page + 1); - this.channelListScroll.current?.scrollTo({top: 0}); - } - - previousPage = (e) => { - e.preventDefault(); - this.setState({page: this.state.page - 1}); - this.channelListScroll.current?.scrollTo({top: 0}); - } - - doSearch = () => { - this.props.search(this.state.channelSearchValue); - if (this.state.channelSearchValue === '') { - this.setState({page: 0}); - } - } - - handleChange = (e) => { - if (e.target) { - this.setState({channelSearchValue: e.target.value}, () => this.doSearch()); - } - } - - handleClear = () => { - this.setState({channelSearchValue: ''}, () => this.doSearch()); - } - - toggleArchivedChannelsOn = () => { - this.props.toggleArchivedChannels(true); - } - - toggleArchivedChannelsOff = () => { - this.props.toggleArchivedChannels(false); - } - - handleChecked = () => { - // If it was checked, and now we're unchecking it, clear the preference - if (this.props.rememberHideJoinedChannelsChecked) { - this.props.hideJoinedChannelsPreference(false); - } else { - this.props.hideJoinedChannelsPreference(true); - } - } - - render() { - const channels = this.props.channels; - let listContent; - let nextButton; - let previousButton; - - let emptyStateMessage = ( - - ); - - if (this.state.channelSearchValue.length > 0) { - emptyStateMessage = ( - - ); - } - - if (this.props.loading && channels.length === 0) { - listContent = ; - } else if (channels.length === 0) { - listContent = ( -
0 ? - localizeAndFormatMessage(t('more_channels.noMore'), 'No results for {text}', {text: this.state.channelSearchValue}) : - localizeMessage('widgets.channels_input.empty', 'No channels found') - } - > - -

- {emptyStateMessage} -

- {this.props.noResultsText} -
- ); - } else { - const pageStart = this.state.page * this.props.channelsPerPage; - const pageEnd = pageStart + this.props.channelsPerPage; - const channelsToDisplay = this.props.channels.slice(pageStart, pageEnd); - listContent = channelsToDisplay.map(this.createChannelRow); - - if (channelsToDisplay.length >= this.props.channelsPerPage && pageEnd < this.props.channels.length) { - nextButton = ( - - ); - } - - if (this.state.page > 0) { - previousButton = ( - - ); - } - } - - const input = ( -
- - -
- ); - - let channelDropdown; - let checkIcon; - - if (this.props.canShowArchivedChannels) { - checkIcon = ( - - ); - channelDropdown = ( - - - {this.props.shouldShowArchivedChannels ? localizeMessage('more_channels.show_archived_channels', 'Channel Type: Archived') : localizeMessage('more_channels.show_public_channels', 'Channel Type: Public')} - - - -
- } - text={localizeMessage('suggestion.search.public', 'Public Channels')} - rightDecorator={this.props.shouldShowArchivedChannels ? null : checkIcon} - ariaLabel={localizeMessage('suggestion.search.public', 'Public Channels')} - /> -
- } - text={localizeMessage('suggestion.archive', 'Archived Channels')} - rightDecorator={this.props.shouldShowArchivedChannels ? checkIcon : null} - ariaLabel={localizeMessage('suggestion.archive', 'Archived Channels')} - /> -
-
- ); - } - - const hideJoinedButtonClass = classNames('get-app__checkbox', {checked: this.props.rememberHideJoinedChannelsChecked}); - const hideJoinedPreferenceCheckbox = ( -
- - -
- ); - - let channelCountLabel; - if (channels.length === 0) { - channelCountLabel = localizeMessage('more_channels.count_zero', '0 Results'); - } else if (channels.length === 1) { - channelCountLabel = localizeMessage('more_channels.count_one', '1 Result'); - } else if (channels.length > 1) { - channelCountLabel = localizeAndFormatMessage(t('more_channels.count'), '0 Results', {count: channels.length}); - } else { - channelCountLabel = localizeMessage('more_channels.count_zero', '0 Results'); - } - - const dropDownContainer = ( -
- {channelCountLabel} -
- {channelDropdown} - {hideJoinedPreferenceCheckbox} -
-
- ); - - return ( -
- {input} - {dropDownContainer} -
-
- {listContent} -
-
-
- {previousButton} - {nextButton} -
-
- ); - } -} - -SearchableChannelList.defaultProps = { - channels: [], - isSearch: false, -}; - -SearchableChannelList.propTypes = { - channels: PropTypes.arrayOf(PropTypes.object), - channelsPerPage: PropTypes.number, - nextPage: PropTypes.func.isRequired, - isSearch: PropTypes.bool, - search: PropTypes.func.isRequired, - handleJoin: PropTypes.func.isRequired, - noResultsText: PropTypes.object, - loading: PropTypes.bool, - toggleArchivedChannels: PropTypes.func.isRequired, - shouldShowArchivedChannels: PropTypes.bool.isRequired, - canShowArchivedChannels: PropTypes.bool.isRequired, - myChannelMemberships: PropTypes.object.isRequired, - allChannelStats: PropTypes.object.isRequired, - closeModal: PropTypes.func.isRequired, - hideJoinedChannelsPreference: PropTypes.func.isRequired, - rememberHideJoinedChannelsChecked: PropTypes.bool.isRequired, -}; -/* eslint-enable react/no-string-refs */ diff --git a/webapp/channels/src/components/searchable_channel_list.jsx b/webapp/channels/src/components/searchable_channel_list.jsx new file mode 100644 index 0000000000..edf7b57565 --- /dev/null +++ b/webapp/channels/src/components/searchable_channel_list.jsx @@ -0,0 +1,319 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components'; + +import LoadingScreen from 'components/loading_screen'; +import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; +import QuickInput from 'components/quick_input'; +import * as UserAgent from 'utils/user_agent'; +import {localizeMessage} from 'utils/utils'; +import LocalizedInput from 'components/localized_input/localized_input'; + +import SharedChannelIndicator from 'components/shared_channel_indicator'; + +import {t} from 'utils/i18n'; + +import MenuWrapper from './widgets/menu/menu_wrapper'; +import Menu from './widgets/menu/menu'; + +const NEXT_BUTTON_TIMEOUT_MILLISECONDS = 500; + +export default class SearchableChannelList extends React.PureComponent { + static getDerivedStateFromProps(props, state) { + return {isSearch: props.isSearch, page: props.isSearch && !state.isSearch ? 0 : state.page}; + } + + constructor(props) { + super(props); + + this.nextTimeoutId = 0; + + this.state = { + joiningChannel: '', + page: 0, + nextDisabled: false, + }; + + this.filter = React.createRef(); + this.channelListScroll = React.createRef(); + } + + componentDidMount() { + // only focus the search box on desktop so that we don't cause the keyboard to open on mobile + if (!UserAgent.isMobile() && this.filter.current) { + this.filter.current.focus(); + } + } + + handleJoin(channel) { + this.setState({joiningChannel: channel.id}); + this.props.handleJoin( + channel, + () => { + this.setState({joiningChannel: ''}); + }, + ); + } + + createChannelRow = (channel) => { + const ariaLabel = `${channel.display_name}, ${channel.purpose}`.toLowerCase(); + let archiveIcon; + let sharedIcon; + const {shouldShowArchivedChannels} = this.props; + + if (shouldShowArchivedChannels) { + archiveIcon = ( + + ); + } + + if (channel.shared) { + sharedIcon = ( + + ); + } + + return ( +
+
+ +

{channel.purpose}

+
+
+ +
+
+ ); + } + + nextPage = (e) => { + e.preventDefault(); + this.setState({page: this.state.page + 1, nextDisabled: true}); + this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT_MILLISECONDS); + this.props.nextPage(this.state.page + 1); + this.channelListScroll.current?.scrollTo({top: 0}); + } + + previousPage = (e) => { + e.preventDefault(); + this.setState({page: this.state.page - 1}); + this.channelListScroll.current?.scrollTo({top: 0}); + } + + doSearch = () => { + const term = this.filter.current.value; + this.props.search(term); + if (term === '') { + this.setState({page: 0}); + } + } + toggleArchivedChannelsOn = () => { + this.props.toggleArchivedChannels(true); + } + toggleArchivedChannelsOff = () => { + this.props.toggleArchivedChannels(false); + } + + render() { + const channels = this.props.channels; + let listContent; + let nextButton; + let previousButton; + + if (this.props.loading && channels.length === 0) { + listContent = ; + } else if (channels.length === 0) { + listContent = ( +
+

+ +

+ {this.props.noResultsText} +
+ ); + } else { + const pageStart = this.state.page * this.props.channelsPerPage; + const pageEnd = pageStart + this.props.channelsPerPage; + const channelsToDisplay = this.props.channels.slice(pageStart, pageEnd); + listContent = channelsToDisplay.map(this.createChannelRow); + + if (channelsToDisplay.length >= this.props.channelsPerPage && pageEnd < this.props.channels.length) { + nextButton = ( + + ); + } + + if (this.state.page > 0) { + previousButton = ( + + ); + } + } + + let input = ( +
+
+ +
+
+ ); + + if (this.props.createChannelButton) { + input = ( +
+
+ +
+
+ {this.props.createChannelButton} +
+
+ ); + } + + let channelDropdown; + + if (this.props.canShowArchivedChannels) { + channelDropdown = ( + + ); + } + + return ( +
+ {input} + {channelDropdown} +
+
+ {listContent} +
+
+
+ {previousButton} + {nextButton} +
+
+ ); + } +} + +SearchableChannelList.defaultProps = { + channels: [], + isSearch: false, +}; + +SearchableChannelList.propTypes = { + channels: PropTypes.arrayOf(PropTypes.object), + channelsPerPage: PropTypes.number, + nextPage: PropTypes.func.isRequired, + isSearch: PropTypes.bool, + search: PropTypes.func.isRequired, + handleJoin: PropTypes.func.isRequired, + noResultsText: PropTypes.object, + loading: PropTypes.bool, + createChannelButton: PropTypes.element, + toggleArchivedChannels: PropTypes.func.isRequired, + shouldShowArchivedChannels: PropTypes.bool.isRequired, + canShowArchivedChannels: PropTypes.bool.isRequired, +}; diff --git a/webapp/channels/src/components/more_channels/searchable_channel_list.test.jsx b/webapp/channels/src/components/searchable_channel_list.test.jsx similarity index 70% rename from webapp/channels/src/components/more_channels/searchable_channel_list.test.jsx rename to webapp/channels/src/components/searchable_channel_list.test.jsx index 9953b08225..9ba5ddd4f8 100644 --- a/webapp/channels/src/components/more_channels/searchable_channel_list.test.jsx +++ b/webapp/channels/src/components/searchable_channel_list.test.jsx @@ -4,25 +4,20 @@ import React from 'react'; import {shallow} from 'enzyme'; -import SearchableChannelList from './searchable_channel_list.jsx'; +import SearchableChannelList from 'components/searchable_channel_list.jsx'; describe('components/SearchableChannelList', () => { const baseProps = { channels: [], isSearch: false, channelsPerPage: 10, - nextPage: jest.fn(), - search: jest.fn(), - handleJoin: jest.fn(), + nextPage: () => {}, // eslint-disable-line no-empty-function + search: () => {}, // eslint-disable-line no-empty-function + handleJoin: () => {}, // eslint-disable-line no-empty-function loading: true, - rememberHideJoinedChannelsChecked: false, - toggleArchivedChannels: jest.fn(), + toggleArchivedChannels: () => {}, // eslint-disable-line no-empty-function shouldShowArchivedChannels: false, canShowArchivedChannels: false, - myChannelMemberships: {}, - allChannelStats: {}, - closeModal: jest.fn(), - hideJoinedChannelsPreference: jest.fn(), }; test('should match init snapshot', () => { diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 6c28bff86c..f84245884b 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -4155,22 +4155,13 @@ "modal.manual_status.title_dnd": "Your Status is Set to \"Do Not Disturb\"", "modal.manual_status.title_offline": "Your Status is Set to \"Offline\"", "modal.manual_status.title_ooo": "Your Status is Set to \"Out of Office\"", - "more_channels.channel_purpose": "Channel Information: Membership Indicator: Joined, Member count {memberCount} , Purpose: {channelPurpose}", - "more_channels.count": "{count} Results", - "more_channels.count_one": "1 Result", - "more_channels.count_zero": "0 Results", "more_channels.create": "Create New Channel", - "more_channels.hide_joined": "Hide Joined", - "more_channels.hide_joined_checked": "Hide joined channels checkbox, checked", - "more_channels.hide_joined_not_checked": "Hide joined channels checkbox, not checked", - "more_channels.joined": "Joined", - "more_channels.membership_indicator": "Membership Indicator: Joined", + "more_channels.createClick": "Click 'Create New Channel' to make a new one", + "more_channels.join": "Join", + "more_channels.joining": "Joining...", "more_channels.next": "Next", - "more_channels.noArchived": "No archived channels", "more_channels.noMore": "No results for \"{text}\"", - "more_channels.noPublic": "No public channels", "more_channels.prev": "Previous", - "more_channels.searchError": "Try searching different keywords, checking for typos or adjusting the filters.", "more_channels.show_archived_channels": "Channel Type: Archived", "more_channels.show_public_channels": "Channel Type: Public", "more_channels.title": "Browse Channels", diff --git a/webapp/channels/src/sass/components/_channel-invite-modal.scss b/webapp/channels/src/sass/components/_channel-invite-modal.scss index d3e70e8398..f1893c9a76 100644 --- a/webapp/channels/src/sass/components/_channel-invite-modal.scss +++ b/webapp/channels/src/sass/components/_channel-invite-modal.scss @@ -71,9 +71,7 @@ .primary-message { margin: 0; - color: var(--center-channel-color); font-size: inherit; - line-height: 28px; } } diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index d3426a4395..5a5825ec5a 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -896,7 +896,6 @@ export const StoragePrefixes = { CHANNEL_CATEGORY_COLLAPSED: 'channelCategoryCollapsed_', INLINE_IMAGE_VISIBLE: 'isInlineImageVisible_', DELINQUENCY: 'delinquency_', - HIDE_JOINED_CHANNELS: 'hideJoinedChannels', }; export const LandingPreferenceTypes = { diff --git a/webapp/platform/components/src/generic_modal/generic_modal.tsx b/webapp/platform/components/src/generic_modal/generic_modal.tsx index 117d1d4b58..9f5ea7a357 100644 --- a/webapp/platform/components/src/generic_modal/generic_modal.tsx +++ b/webapp/platform/components/src/generic_modal/generic_modal.tsx @@ -38,7 +38,6 @@ export type Props = { compassDesign?: boolean; backdrop?: boolean; backdropClassName?: string; - headerButton?: React.ReactNode; tabIndex?: number; children: React.ReactNode; keyboardEscape?: boolean; @@ -166,7 +165,6 @@ export class GenericModal extends React.PureComponent {

{this.props.modalHeaderText}

- {this.props.headerButton}
);