diff --git a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js index d62984cca0..e2126cd3b8 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js @@ -146,7 +146,7 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('.more-modal').should('be.visible').within(() => { + cy.get('#moreChannelsModal').should('be.visible').within(() => { // # Public channel list opens by default cy.findByText(channelType.public).should('be.visible').click(); @@ -199,7 +199,7 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('.more-modal').should('be.visible').within(() => { + cy.get('#moreChannelsModal').should('be.visible').within(() => { // # Public channels are shown by default cy.findByText(channelType.public).should('be.visible').click(); @@ -253,7 +253,7 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('.more-modal').should('be.visible').within(() => { + cy.get('#moreChannelsModal').should('be.visible').within(() => { // # Show public channels is visible by default cy.findByText(channelType.public).should('be.visible').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index d7fd550e37..768508b27e 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -85,8 +85,8 @@ describe('Channels', () => { }); }); - // # Verify that the modal is closed and it's redirected to the selected channel - cy.get('#moreChannelsModal').should('not.exist'); + // # Verify that the modal is not closed + cy.get('#moreChannelsModal').should('exist'); cy.url().should('include', `/${testTeam.name}/channels/${testChannel.name}`); // # Login as channel admin and go directly to the channel diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js index 8624b60edf..381d621e6a 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js @@ -11,8 +11,7 @@ // Group: @channels @channel function verifyNoChannelToJoinMessage(isVisible) { - 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'); + cy.findByText('No public channels').should(isVisible ? 'be.visible' : 'not.exist'); } describe('more public channels', () => { @@ -53,7 +52,10 @@ describe('more public channels', () => { cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Assert that the moreChannelsModel is visible - cy.findByRole('dialog', {name: 'More Channels'}).should('be.visible').within(() => { + cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => { + // # Click hide joined checkbox + cy.findByText('Hide Joined').should('be.visible').click(); + // * Assert that the moreChannelsList is visible and the number of channels is 31 cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 31); @@ -86,10 +88,7 @@ describe('more public channels', () => { cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Assert the moreChannelsModel is visible - 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); - + cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => { // * Assert that the "No more channels to join" message is visible verifyNoChannelToJoinMessage(true); }); diff --git a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts index c1eb246381..777966ed97 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts +++ b/e2e-tests/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('.more-modal').should('be.visible'); + cy.get('#moreChannelsModal').should('be.visible'); // Click the Off-Topic channel - cy.get('.more-modal button:contains(Off-Topic)').should('be.visible').click(); + cy.findByText('Off-Topic').should('be.visible').click(); // Verify that new channel is in the sidebar and is active - cy.get('.more-modal').should('not.exist'); + cy.get('#moreChannelsModal').should('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-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js index ae6df74721..8483be2867 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js @@ -104,30 +104,26 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => { cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Verify the accessibility support in More Channels Dialog - cy.findByRole('dialog', {name: 'More Channels'}).within(() => { - cy.findByRole('heading', {name: 'More Channels'}); + cy.findByRole('dialog', {name: 'Browse Channels'}).within(() => { + cy.findByRole('heading', {name: 'Browse Channels'}); // * Verify the accessibility support in search input cy.findByPlaceholderText('Search channels'); - cy.waitUntil(() => cy.get('#moreChannelsList').then((el) => { + cy.get('#moreChannelsList').should('be.visible').then((el) => { return el[0].children.length === 2; - })); + }); - // # Focus on the Create Channel button and TAB twice - cy.get('#createNewChannel').focus().tab().tab(); + // # 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(); // * Verify channel name is highlighted and reader reads the channel name and channel description - cy.get('#moreChannelsList').children().eq(0).within(() => { + cy.get('#moreChannelsList').within(() => { const selectedChannel = getChannelAriaLabel(channel); - 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'); + cy.findByLabelText(selectedChannel).should('be.visible').should('be.focused'); }); // * Press Tab again and verify if focus changes to next row diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js index a509642532..ed73e0f277 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js +++ b/e2e-tests/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 more channels to join'); + cy.get('#moreChannelsList').should('include.text', 'No results for'); }); 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 more channels to join'); + cy.get('#moreChannelsList').should('include.text', 'No results for'); }); }); diff --git a/webapp/channels/src/actions/channel_actions.test.ts b/webapp/channels/src/actions/channel_actions.test.ts index 6b247396b2..aac40512ec 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)); + await testStore.dispatch(searchMoreChannels('', false, true)); expect(testStore.getActions()).toEqual(expectedActions); }); diff --git a/webapp/channels/src/actions/channel_actions.ts b/webapp/channels/src/actions/channel_actions.ts index b3c9ffb156..0add017514 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): ActionFunc { +export function searchMoreChannels(term: string, showArchivedChannels: boolean, hideJoinedChannels: boolean): ActionFunc { return async (dispatch, getState) => { const state = getState(); const teamId = getCurrentTeamId(state); @@ -121,9 +121,7 @@ export function searchMoreChannels(term: string, showArchivedChannels: boolean): const {data, error} = await dispatch(ChannelActions.searchChannels(teamId, term, showArchivedChannels)); if (data) { const myMembers = getMyChannelMemberships(state); - - // 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]); + const channels = hideJoinedChannels ? (data as Channel[]).filter((channel) => !myMembers[channel.id]) : data; 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 index eff77678a4..1118d16bf2 100644 --- a/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.jsx.snap +++ b/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.jsx.snap @@ -7,34 +7,70 @@ exports[`components/SearchableChannelList should match init snapshot 1`] = `
- +
+ + 0 Results + +
+
+
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 new file mode 100644 index 0000000000..75b2ec8d3d --- /dev/null +++ b/webapp/channels/src/components/common/svg_images_components/magnifying_glass_svg.tsx @@ -0,0 +1,39 @@ +// 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/more_channels/__snapshots__/more_channels.test.tsx.snap b/webapp/channels/src/components/more_channels/__snapshots__/more_channels.test.tsx.snap index fe1acbf101..eb0a190f2c 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,53 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/MoreChannels should match snapshot and state 1`] = ` - - - - - - - - - - + } + id="moreChannelsModal" + keyboardEscape={true} + modalHeaderText={ + + } + onExited={[Function]} + show={true} +> + +

+ +

-

- -

+ +
- } - search={[Function]} - shouldShowArchivedChannels={false} - toggleArchivedChannels={[Function]} - /> -
-
+ + } + rememberHideJoinedChannelsChecked={false} + search={[Function]} + shouldShowArchivedChannels={false} + toggleArchivedChannels={[Function]} + /> + `; diff --git a/webapp/channels/src/components/more_channels/index.ts b/webapp/channels/src/components/more_channels/index.ts index 80bfd4a8c1..ff084cb172 100644 --- a/webapp/channels/src/components/more_channels/index.ts +++ b/webapp/channels/src/components/more_channels/index.ts @@ -12,7 +12,7 @@ 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} from 'mattermost-redux/actions/channels'; -import {getOtherChannels, getChannelsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels'; +import {getChannelsInCurrentTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels'; import {searchMoreChannels} from 'actions/channel_actions'; import {openModal, closeModal} from 'actions/views/modals'; @@ -24,11 +24,14 @@ import {ModalData} from 'types/actions'; import {GlobalState} from 'types/store'; import MoreChannels from './more_channels'; +import {makeGetGlobalItem} from 'selectors/storage'; +import Constants, {StoragePrefixes} from 'utils/constants'; +import {setGlobalItem} from 'actions/storage'; -const getNotArchivedOtherChannels = createSelector( - 'getNotArchivedOtherChannels', - getOtherChannels, - (channels: Channel[]) => channels && channels.filter((c) => c.delete_at === 0), +const getChannelsWithoutArchived = createSelector( + 'getChannelsWithoutArchived', + getChannelsInCurrentTeam, + (channels: Channel[]) => channels && channels.filter((c) => c.delete_at === 0 && c.type !== Constants.PRIVATE_CHANNEL), ); const getArchivedOtherChannels = createSelector( @@ -39,15 +42,18 @@ const getArchivedOtherChannels = createSelector( function mapStateToProps(state: GlobalState) { const team = getCurrentTeam(state) || {}; + const getGlobalItem = makeGetGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, 'false'); return { - channels: getNotArchivedOtherChannels(state) || [], + channels: getChannelsWithoutArchived(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) || {}, + shouldHideJoinedChannels: getGlobalItem(state) === 'true', rhsState: getRhsState(state), rhsOpen: getIsRhsOpen(state), }; @@ -60,6 +66,7 @@ type Actions = { searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean) => Promise; openModal:

(modalData: ModalData

) => void; closeModal: (modalId: string) => void; + setGlobalItem: (name: string, value: string) => void; closeRightHandSide: () => void; } @@ -72,6 +79,7 @@ function mapDispatchToProps(dispatch: Dispatch) { searchMoreChannels, openModal, closeModal, + 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 new file mode 100644 index 0000000000..56312debbe --- /dev/null +++ b/webapp/channels/src/components/more_channels/more_channels.scss @@ -0,0 +1,297 @@ +@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; + padding: 0 32px; + + .input-clear { + top: 16px; + right: 46px; + } + + #searchIcon { + position: absolute; + top: 16px; + left: 46px; + 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; + border: none; + background-color: unset; + } + + .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; // todo check + 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 a591bfef5b..aa0b9a1021 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/searchable_channel_list.jsx'; +import SearchableChannelList from 'components/searchable_channel_list'; import {getHistory} from 'utils/browser_history'; import {TestHelper} from 'utils/test_helper'; @@ -73,6 +73,8 @@ describe('components/MoreChannels', () => { teamName: 'team_name', channelsRequestStarted: false, canShowArchivedChannels: true, + shouldHideJoinedChannels: false, + myChannelMemberships: {}, actions: { getChannels: jest.fn(), getArchivedChannels: jest.fn(), @@ -81,6 +83,7 @@ describe('components/MoreChannels', () => { openModal: jest.fn(), closeModal: jest.fn(), closeRightHandSide: jest.fn(), + setGlobalItem: jest.fn(), }, }; @@ -91,7 +94,6 @@ 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(); @@ -102,16 +104,6 @@ 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( , @@ -211,7 +203,6 @@ describe('components/MoreChannels', () => { process.nextTick(() => { expect(getHistory().push).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledTimes(1); - expect(wrapper.state('show')).toEqual(false); done(); }); }); @@ -249,7 +240,7 @@ describe('components/MoreChannels', () => { jest.runOnlyPendingTimers(); expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('fail', false); + expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('fail', false, false); process.nextTick(() => { expect(wrapper.state('search')).toEqual(true); expect(wrapper.state('searching')).toEqual(false); @@ -276,7 +267,7 @@ describe('components/MoreChannels', () => { jest.runOnlyPendingTimers(); expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', false); + expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', false, false); process.nextTick(() => { expect(wrapper.state('search')).toEqual(true); expect(wrapper.state('searching')).toEqual(false); @@ -303,7 +294,7 @@ describe('components/MoreChannels', () => { jest.runOnlyPendingTimers(); expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', true); + expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', true, false); process.nextTick(() => { expect(wrapper.state('search')).toEqual(true); expect(wrapper.state('searching')).toEqual(false); diff --git a/webapp/channels/src/components/more_channels/more_channels.tsx b/webapp/channels/src/components/more_channels/more_channels.tsx index de6db52c6a..6c797cc342 100644 --- a/webapp/channels/src/components/more_channels/more_channels.tsx +++ b/webapp/channels/src/components/more_channels/more_channels.tsx @@ -2,23 +2,29 @@ // See LICENSE.txt for license information. import React from 'react'; -import {Modal} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; import {ActionResult} from 'mattermost-redux/types/actions'; -import {Channel} from '@mattermost/types/channels'; +import {RelationOneToOne} from '@mattermost/types/utilities'; +import {Channel, ChannelMembership} from '@mattermost/types/channels'; import Permissions from 'mattermost-redux/constants/permissions'; import NewChannelModal from 'components/new_channel_modal/new_channel_modal'; -import SearchableChannelList from 'components/searchable_channel_list.jsx'; +import SearchableChannelList from 'components/searchable_channel_list'; import TeamPermissionGate from 'components/permissions_gates/team_permission_gate'; import {ModalData} from 'types/actions'; import {RhsState} from 'types/store/rhs'; import {getHistory} from 'utils/browser_history'; -import {ModalIdentifiers, RHSStates} from 'utils/constants'; +import {ModalIdentifiers, RHSStates, StoragePrefixes} from 'utils/constants'; import {getRelativeChannelURL} from 'utils/url'; +import {GenericModal} from '@mattermost/components'; +import classNames from 'classnames'; +import {localizeMessage} from 'utils/utils'; +import LoadingScreen from 'components/loading_screen'; + +import './more_channels.scss'; const CHANNELS_CHUNK_SIZE = 50; const CHANNELS_PER_PAGE = 50; @@ -28,9 +34,14 @@ 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) => Promise; + searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean, shouldHideJoinedChannels: boolean) => Promise; openModal:

(modalData: ModalData

) => void; closeModal: (modalId: string) => void; + + /* + * Function to set a key-value pair in the local storage + */ + setGlobalItem: (name: string, value: string) => void; closeRightHandSide: () => void; } @@ -43,13 +54,15 @@ export type Props = { channelsRequestStarted?: boolean; canShowArchivedChannels?: boolean; morePublicChannelsModalType?: string; + myChannelMemberships: RelationOneToOne; + shouldHideJoinedChannels: boolean; rhsState?: RhsState; rhsOpen?: boolean; actions: Actions; } type State = { - show: boolean; + loading: boolean; shouldShowArchivedChannels: boolean; search: boolean; searchedChannels: Channel[]; @@ -60,6 +73,7 @@ type State = { export default class MoreChannels extends React.PureComponent { public searchTimeoutId: number; + activeChannels: Channel[] = []; constructor(props: Props) { super(props); @@ -67,7 +81,7 @@ export default class MoreChannels extends React.PureComponent { this.searchTimeoutId = 0; this.state = { - show: true, + loading: true, shouldShowArchivedChannels: this.props.morePublicChannelsModalType === 'private', search: false, searchedChannels: [], @@ -82,10 +96,11 @@ export default class MoreChannels extends React.PureComponent { if (this.props.canShowArchivedChannels) { this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2); } + this.loadComplete(); } - handleHide = () => { - this.setState({show: false}); + loadComplete = () => { + this.setState({loading: false}); }; handleNewChannel = () => { @@ -124,14 +139,17 @@ export default class MoreChannels extends React.PureComponent { handleJoin = async (channel: Channel, done: () => void) => { const {actions, currentUserId, teamId, teamName} = this.props; - const result = await actions.joinChannel(currentUserId, teamId, channel.id); + let result; - if (result.error) { + if (!this.isMemberOfChannel(channel.id)) { + result = await actions.joinChannel(currentUserId, teamId, channel.id); + } + + if (result?.error) { this.setState({serverError: result.error.message}); } else { getHistory().push(getRelativeChannelURL(teamName, channel.name)); this.closeEditRHS(); - this.handleHide(); } if (done) { @@ -153,7 +171,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); + const {data} = await this.props.actions.searchMoreChannels(term, this.state.shouldShowArchivedChannels, this.props.shouldHideJoinedChannels); if (searchTimeoutId !== this.searchTimeoutId) { return; } @@ -183,29 +201,47 @@ 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; - let activeChannels; + const otherChannelsWithoutJoined = channels.filter((channel) => !this.isMemberOfChannel(channel.id)); + const archivedChannelsWithoutJoined = archivedChannels.filter((channel) => !this.isMemberOfChannel(channel.id)); - if (shouldShowArchivedChannels) { - activeChannels = search ? searchedChannels : archivedChannels; + 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; } else { - activeChannels = search ? searchedChannels : channels; + this.activeChannels = search ? searchedChannels : channels; } let serverError; @@ -214,87 +250,87 @@ export default class MoreChannels extends React.PureComponent {

; } - const createNewChannelButton = ( - - - - ); + + + ); + }; - const createChannelHelpText = ( - + const noResultsText = ( + <>

-
+ {createNewChannelButton('primaryButton', )} + ); - const body = ( + const body = this.state.loading ? : ( {serverError} ); + const title = ( + + ); + return ( - - - - - - {createNewChannelButton} - - - {body} - - + {body} + ); } } diff --git a/webapp/channels/src/components/searchable_channel_list.jsx b/webapp/channels/src/components/searchable_channel_list.jsx deleted file mode 100644 index fc66227ba7..0000000000 --- a/webapp/channels/src/components/searchable_channel_list.jsx +++ /dev/null @@ -1,319 +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 {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/searchable_channel_list.test.jsx b/webapp/channels/src/components/searchable_channel_list.test.jsx index 9ba5ddd4f8..1c5e58d10f 100644 --- a/webapp/channels/src/components/searchable_channel_list.test.jsx +++ b/webapp/channels/src/components/searchable_channel_list.test.jsx @@ -4,20 +4,24 @@ import React from 'react'; import {shallow} from 'enzyme'; -import SearchableChannelList from 'components/searchable_channel_list.jsx'; +import SearchableChannelList from 'components/searchable_channel_list'; describe('components/SearchableChannelList', () => { const baseProps = { channels: [], isSearch: false, channelsPerPage: 10, - nextPage: () => {}, // eslint-disable-line no-empty-function - search: () => {}, // eslint-disable-line no-empty-function - handleJoin: () => {}, // eslint-disable-line no-empty-function + nextPage: jest.fn(), + search: jest.fn(), + handleJoin: jest.fn(), loading: true, - toggleArchivedChannels: () => {}, // eslint-disable-line no-empty-function + toggleArchivedChannels: jest.fn(), + closeModal: jest.fn(), + hideJoinedChannelsPreference: jest.fn(), shouldShowArchivedChannels: false, canShowArchivedChannels: false, + rememberHideJoinedChannelsChecked: false, + noResultsText: <>{'no channel found'}, }; test('should match init snapshot', () => { diff --git a/webapp/channels/src/components/searchable_channel_list.tsx b/webapp/channels/src/components/searchable_channel_list.tsx new file mode 100644 index 0000000000..aa395d29ce --- /dev/null +++ b/webapp/channels/src/components/searchable_channel_list.tsx @@ -0,0 +1,481 @@ +// 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 {ArchiveOutlineIcon, CheckIcon, ChevronDownIcon, GlobeIcon, LockOutlineIcon, MagnifyIcon} from '@mattermost/compass-icons/components'; +import {Channel, ChannelMembership} from '@mattermost/types/channels'; +import {RelationOneToOne} from '@mattermost/types/utilities'; +import {isPrivateChannel} from 'mattermost-redux/utils/channel_utils'; + +import classNames from 'classnames'; + +import LoadingScreen from 'components/loading_screen'; +import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; +import QuickInput from 'components/quick_input'; +import CheckboxCheckedIcon from 'components/widgets/icons/checkbox_checked_icon'; +import LocalizedInput from 'components/localized_input/localized_input'; +import MagnifyingGlassSVG from 'components/common/svg_images_components/magnifying_glass_svg'; + +import * as UserAgent from 'utils/user_agent'; +import Constants, {ModalIdentifiers} from 'utils/constants'; +import {localizeMessage, localizeAndFormatMessage} from 'utils/utils'; +import {isArchivedChannel} from 'utils/channel_utils'; + +import {t} from 'utils/i18n'; +import MenuWrapper from './widgets/menu/menu_wrapper'; +import Menu from './widgets/menu/menu'; +import {isKeyPressed} from 'utils/keyboard'; + +const NEXT_BUTTON_TIMEOUT_MILLISECONDS = 500; + +type Props = { + channels: Channel[]; + channelsPerPage: number; + nextPage: (page: number) => void; + isSearch: boolean; + search: (term: string) => void; + handleJoin: (channel: Channel, done: () => void) => void; + noResultsText: JSX.Element; + toggleArchivedChannels: (shouldShowArchivedChannels: boolean) => void; + shouldShowArchivedChannels: boolean; + myChannelMemberships: RelationOneToOne; + closeModal: (modalId: string) => void; + hideJoinedChannelsPreference: (shouldHideJoinedChannels: boolean) => void; + rememberHideJoinedChannelsChecked: boolean; + canShowArchivedChannels?: boolean; + loading?: boolean; +} + +type State = { + joiningChannel: string; + page: number; + nextDisabled: boolean; + channelSearchValue: string; + isSearch?: boolean; +} + +export default class SearchableChannelList extends React.PureComponent { + private nextTimeoutId: number | NodeJS.Timeout; + private filter: React.RefObject; + private channelListScroll: React.RefObject; + + static getDerivedStateFromProps(props: Props, state: State) { + return {isSearch: props.isSearch, page: props.isSearch && !state.isSearch ? 0 : state.page}; + } + + constructor(props: 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: KeyboardEvent) => { + const target = e.target as HTMLElement; + 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: Channel, e: React.MouseEvent) => { + 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: string) { + return this.props.myChannelMemberships[channelId]; + } + + createChannelRow = (channel: Channel) => { + const ariaLabel = `${channel.display_name}, ${channel.purpose}`.toLowerCase(); + let channelTypeIcon; + + 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, Purpose: {channelPurpose}', + {channelPurpose: channel.purpose || ''}, + ); + + const channelPurposeContainer = ( +
+ {membershipIndicator} + {(channel.purpose.length > 0 && membershipIndicator) ? : null} + {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: React.MouseEvent) => { + 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: React.MouseEvent) => { + 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?: React.FormEvent) => { + if (e?.currentTarget) { + this.setState({channelSearchValue: e?.currentTarget.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 = ( + + + +
+ } + 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, +// }; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 9b72fe0858..227cb88032 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -4096,16 +4096,25 @@ "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.createClick": "Click 'Create New Channel' to make a new one", - "more_channels.join": "Join", - "more_channels.joining": "Joining...", + "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.next": "Next", - "more_channels.noMore": "No more channels to join", + "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": "More Channels", + "more_channels.title": "Browse Channels", "more_channels.view": "View", "more_direct_channels.directchannel.deactivated": "{displayname} - Deactivated", "more_direct_channels.directchannel.you": "{displayname} (you)", diff --git a/webapp/channels/src/sass/components/_channel-invite-modal.scss b/webapp/channels/src/sass/components/_channel-invite-modal.scss index f1893c9a76..d3e70e8398 100644 --- a/webapp/channels/src/sass/components/_channel-invite-modal.scss +++ b/webapp/channels/src/sass/components/_channel-invite-modal.scss @@ -71,7 +71,9 @@ .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 f3aa701f3d..686ee962eb 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -904,6 +904,7 @@ 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 11521ea565..7e65fbfdc2 100644 --- a/webapp/platform/components/src/generic_modal/generic_modal.tsx +++ b/webapp/platform/components/src/generic_modal/generic_modal.tsx @@ -47,6 +47,7 @@ export type Props = { bodyPadding?: boolean; footerContent?: React.ReactNode; footerDivider?: boolean; + headerButton?: React.ReactNode; }; type State = { @@ -173,6 +174,7 @@ export class GenericModal extends React.PureComponent {

{this.props.modalHeaderText}

+ {this.props.headerButton}
);