diff --git a/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts index 513d5c5f330..8c4c4575b95 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts @@ -245,11 +245,21 @@ describe('Channels', () => { cy.wrap(el).should('contain', channelType.all); }); - // * Users should be able to type and search + // * Hide Archived is on by default, so search only returns active matches + cy.get('#hideArchivedPreferenceCheckbox').should('be.visible').and('have.attr', 'aria-checked', 'true'); cy.get('#searchChannelsTextbox').should('be.visible').type('iv').wait(TIMEOUTS.HALF_SEC); + cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1); + cy.get('#moreChannelsList').should('be.visible').within(() => { + cy.findByText(newChannel.display_name).should('be.visible'); + cy.findByText(testArchivedChannel.display_name).should('not.exist'); + }); + + // # Uncheck Hide Archived so archived matches appear in All search results + cy.get('#hideArchivedPreferenceCheckbox').click(); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); cy.get('#moreChannelsList').should('be.visible').within(() => { cy.findByText(newChannel.display_name).should('be.visible'); + cy.findByText(testArchivedChannel.display_name).should('be.visible'); }); cy.get('#browseChannelsModal').should('be.visible'); diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/browse_channels_modal.ts b/e2e-tests/playwright/lib/src/ui/components/channels/browse_channels_modal.ts index b539aff730b..f5968a6f293 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/browse_channels_modal.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/browse_channels_modal.ts @@ -8,6 +8,7 @@ export default class BrowseChannelsModal { readonly container: Locator; readonly createNewChannelButton: Locator; + readonly hideArchivedCheckbox: Locator; readonly hideJoinedCheckbox: Locator; readonly searchInput: Locator; @@ -17,6 +18,7 @@ export default class BrowseChannelsModal { this.container = container; this.createNewChannelButton = container.getByRole('button', {name: 'Create New Channel'}); + this.hideArchivedCheckbox = container.getByRole('checkbox', {name: 'Hide Archived'}); this.hideJoinedCheckbox = container.getByRole('checkbox', {name: 'Hide Joined'}); this.searchInput = container.getByRole('textbox', {name: 'Search channels'}); diff --git a/e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts index 2583ecbab75..908e899617b 100644 --- a/e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts +++ b/e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts @@ -55,6 +55,7 @@ test( await hideJoinedCheckbox.click(); // # Focus on Create Channel button and tab through elements + // Tab order: Close → Search → Channel type filter → Hide Archived → Hide Joined → first channel const createChannelButton = dialog.createNewChannelButton; await createChannelButton.focus(); await page.keyboard.press('Tab'); @@ -62,6 +63,7 @@ test( await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); // * Verify channel name is highlighted and has proper aria-label await dialog.toHaveChannelAsNthResult(channel1.name, 0); @@ -116,6 +118,7 @@ test( - /status: \\d+ Results/ - status: Channel type filter set to All - button "Channel type filter" + - checkbox "Hide archived channels" [checked]: Hide Archived - checkbox "Hide joined channels": Hide Joined - search `); diff --git a/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.tsx.snap b/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.tsx.snap index aa2f6cca105..39deb4f4f83 100644 --- a/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.tsx.snap +++ b/webapp/channels/src/components/__snapshots__/searchable_channel_list.test.tsx.snap @@ -75,6 +75,74 @@ exports[`components/SearchableChannelList should match init snapshot 1`] = ` /> +
+
{ teamName: 'team_name', channelsRequestStarted: false, shouldHideJoinedChannels: false, + shouldHideArchivedChannels: true, accessControlEnabled: false, myChannelMemberships: { 'channel-id-3': TestHelper.getChannelMembershipMock({ @@ -714,6 +715,230 @@ describe('components/BrowseChannels', () => { expect(baseProps.actions.getRecommendedChannelsForUser).not.toHaveBeenCalled(); }); + test('hides archived channels from All search results when shouldHideArchivedChannels is true', async () => { + const searchAllChannels = jest.fn(channelActions.searchAllChannels); + const props = {...baseProps, shouldHideArchivedChannels: true, actions: {...baseProps.actions, searchAllChannels}}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + const searchInput = screen.getByPlaceholderText('Search channels'); + await user.type(searchInput, 'channel'); + + await act(async () => { + jest.runOnlyPendingTimers(); + await Promise.resolve(); + }); + + // The active public and private channels are shown, but the archived + // channel returned by the search is filtered out under the default + // "All" filter — proving only archived rows are removed. + await waitFor(() => { + expect(screen.getByText('Channel 1')).toBeInTheDocument(); + expect(screen.getByText('Private')).toBeInTheDocument(); + }); + expect(screen.queryByText('Archived')).not.toBeInTheDocument(); + }); + + test('hides archived channels from the browse list by default (no search)', async () => { + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + // baseProps.shouldHideArchivedChannels is true, so the archived channel + // ('channel-2') is absent from the default All browse list. + expect(screen.getByText('Default Channel')).toBeInTheDocument(); + expect(screen.queryByText('channel-2')).not.toBeInTheDocument(); + }); + + test('shows archived channels in the browse list when shouldHideArchivedChannels is false (no search)', async () => { + const props = {...baseProps, shouldHideArchivedChannels: false}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + // With the toggle off, the archived channel is mixed into the All list. + expect(screen.getByText('Default Channel')).toBeInTheDocument(); + expect(screen.getByText('channel-2')).toBeInTheDocument(); + }); + + test('shows archived channels in All search results when shouldHideArchivedChannels is false', async () => { + const searchAllChannels = jest.fn(channelActions.searchAllChannels); + const props = {...baseProps, shouldHideArchivedChannels: false, actions: {...baseProps.actions, searchAllChannels}}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + const searchInput = screen.getByPlaceholderText('Search channels'); + await user.type(searchInput, 'channel'); + + await act(async () => { + jest.runOnlyPendingTimers(); + await Promise.resolve(); + }); + + // With the toggle off, archived channels are mixed back into the results. + await waitFor(() => { + expect(screen.getByText('Channel 1')).toBeInTheDocument(); + expect(screen.getByText('Archived')).toBeInTheDocument(); + }); + }); + + // Regression: archived private channels used to leak in via the + // privateChannels list (which is not filtered by the Hide Archived toggle) + // and, with the toggle off, appear twice — once from privateChannels and + // once from archivedChannels. The container selector now returns only + // active private channels, so archived channels of both types come solely + // from `archivedChannels`. + const archivedPublicChannel = TestHelper.getChannelMock({ + id: 'archived-public-id', + team_id: 'team_1', + display_name: 'Archived Public', + name: 'archived-public', + type: 'O', + delete_at: 123, + }); + + const archivedPrivateChannel = TestHelper.getChannelMock({ + id: 'archived-private-id', + team_id: 'team_1', + display_name: 'Archived Private', + name: 'archived-private', + type: 'P', + delete_at: 456, + }); + + const bothTypesArchivedProps: Props = { + ...baseProps, + channels: [defaultChannel], + privateChannels: [privateChannel], + archivedChannels: [archivedPublicChannel, archivedPrivateChannel], + }; + + test('hides both archived public and private channels from the All list when the toggle is on', async () => { + const props = {...bothTypesArchivedProps, shouldHideArchivedChannels: true}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + expect(screen.getByText('Default Channel')).toBeInTheDocument(); + expect(screen.queryByText('Archived Public')).not.toBeInTheDocument(); + expect(screen.queryByText('Archived Private')).not.toBeInTheDocument(); + }); + + test('shows each archived channel exactly once in the All list when the toggle is off', async () => { + const props = {...bothTypesArchivedProps, shouldHideArchivedChannels: false}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + expect(screen.getByText('Archived Public')).toBeInTheDocument(); + + // Exactly one row — previously the archived private channel rendered + // twice because it came from both privateChannels and archivedChannels. + expect(screen.getAllByText('Archived Private')).toHaveLength(1); + }); + + test('includes archived channels of the matching type under the Public and Private filters when the toggle is off', async () => { + const props = {...bothTypesArchivedProps, shouldHideArchivedChannels: false}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + await user.click(screen.getByLabelText('Channel type filter')); + await user.click(await screen.findByText('Public channels')); + + // Wait on the private row being pruned — "Archived Public" is present in + // both the All and Public views, so it isn't a reliable settle signal. + await waitFor(() => { + expect(screen.queryByText('Archived Private')).not.toBeInTheDocument(); + }); + expect(screen.getByText('Archived Public')).toBeInTheDocument(); + + await user.click(screen.getByLabelText('Channel type filter')); + await user.click(await screen.findByText('Private channels')); + + await waitFor(() => { + expect(screen.queryByText('Archived Public')).not.toBeInTheDocument(); + }); + expect(screen.getByText('Archived Private')).toBeInTheDocument(); + }); + + test('still shows archived channels when the Archived filter is selected even if shouldHideArchivedChannels is true', async () => { + const searchAllChannels = jest.fn(channelActions.searchAllChannels); + const props = {...baseProps, shouldHideArchivedChannels: true, actions: {...baseProps.actions, searchAllChannels}}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + await user.click(screen.getByLabelText('Channel type filter')); + await user.click(await screen.findByText('Archived channels')); + + const searchInput = screen.getByPlaceholderText('Search channels'); + await user.type(searchInput, 'channel'); + + await act(async () => { + jest.runOnlyPendingTimers(); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(screen.getByText('Archived')).toBeInTheDocument(); + }); + expect(screen.queryByText('Channel 1')).not.toBeInTheDocument(); + }); + + test('toggling Hide Archived persists the preference', async () => { + const setGlobalItem = jest.fn(); + const props = {...baseProps, shouldHideArchivedChannels: true, actions: {...baseProps.actions, setGlobalItem}}; + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + // Starts checked (hidden by default); unchecking persists 'false'. + const hideArchived = screen.getByLabelText('Hide archived channels'); + expect(hideArchived).toHaveAttribute('aria-checked', 'true'); + + await user.click(hideArchived); + + expect(setGlobalItem).toHaveBeenCalledWith('hideArchivedChannels', 'false'); + }); + + test('Hide Archived checkbox is not shown when the Archived filter is selected', async () => { + renderWithContext(); + + await act(async () => { + await Promise.resolve(); + }); + + expect(screen.getByLabelText('Hide archived channels')).toBeInTheDocument(); + + await user.click(screen.getByLabelText('Channel type filter')); + await user.click(await screen.findByText('Archived channels')); + + await waitFor(() => { + expect(screen.queryByLabelText('Hide archived channels')).not.toBeInTheDocument(); + }); + }); + // --------------------------------------------------------------- // Discoverable Private Channels — row state machine + filter chips // --------------------------------------------------------------- diff --git a/webapp/channels/src/components/browse_channels/browse_channels.tsx b/webapp/channels/src/components/browse_channels/browse_channels.tsx index 7a04086701d..bbb62d41419 100644 --- a/webapp/channels/src/components/browse_channels/browse_channels.tsx +++ b/webapp/channels/src/components/browse_channels/browse_channels.tsx @@ -87,6 +87,7 @@ export type Props = { channelsRequestStarted?: boolean; myChannelMemberships: RelationOneToOne; shouldHideJoinedChannels: boolean; + shouldHideArchivedChannels: boolean; rhsState?: RhsState; rhsOpen?: boolean; channelsMemberCount?: Record; @@ -363,7 +364,10 @@ export default class BrowseChannels extends React.PureComponent { searchedChannels = channels.filter((c) => c.type === Constants.PRIVATE_CHANNEL && this.canSeePrivateChannel(c)); } if (this.state.filter === Filter.Public) { - searchedChannels = channels.filter((c) => c.type === Constants.OPEN_CHANNEL && c.delete_at === 0); + // Archived public channels are pruned below when the Hide Archived + // toggle is on, so don't force delete_at === 0 here — that would + // hide them even when the toggle is off. + searchedChannels = channels.filter((c) => c.type === Constants.OPEN_CHANNEL); } if (this.state.filter === Filter.Archived) { searchedChannels = channels.filter((c) => c.delete_at !== 0); @@ -385,6 +389,9 @@ export default class BrowseChannels extends React.PureComponent { if (this.state.filter === Filter.MyPendingRequests) { searchedChannels = channels.filter((c) => this.props.myPendingJoinRequests[c.id]); } + if (this.state.filter !== Filter.Archived && this.props.shouldHideArchivedChannels) { + searchedChannels = this.getChannelsWithoutArchived(searchedChannels); + } if (this.props.shouldHideJoinedChannels) { searchedChannels = this.getChannelsWithoutJoined(searchedChannels); } @@ -428,10 +435,18 @@ export default class BrowseChannels extends React.PureComponent { this.props.actions.setGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, shouldHideJoinedChannels.toString()); }; + handleShowArchivedChannelsPreference = (shouldHideArchivedChannels: boolean) => { + // search again when toggling to update search results + this.search(this.state.searchTerm); + this.props.actions.setGlobalItem(StoragePrefixes.HIDE_ARCHIVED_CHANNELS, shouldHideArchivedChannels.toString()); + }; + getChannelsWithoutJoined = (channelList: Channel[]) => channelList.filter((channel) => !this.isMemberOfChannel(channel.id)); + getChannelsWithoutArchived = (channelList: Channel[]) => channelList.filter((channel) => channel.delete_at === 0); + getActiveChannels = () => { - const {channels, archivedChannels, shouldHideJoinedChannels, privateChannels, myPendingJoinRequests} = this.props; + const {channels, archivedChannels, shouldHideJoinedChannels, shouldHideArchivedChannels, privateChannels, myPendingJoinRequests} = this.props; const {search, searchedChannels, filter, recommendedChannels, discoverableChannels} = this.state; // Discoverable private channels the user is not yet a member of. These @@ -456,11 +471,23 @@ export default class BrowseChannels extends React.PureComponent { // appear in the default browse view, not only under the Discoverable // filter. privateChannels-sourced rows are already in allChannels. const extraDiscoverable = discoverableChannels.filter((c) => !privateChannels.some((p) => p.id === c.id)); - const allChannels = channels.concat(privateChannels, extraDiscoverable).sort((a, b) => a.display_name.localeCompare(b.display_name)); + + // `archivedChannels` holds both archived public and private channels. + // When the toggle is on they are dropped everywhere except the explicit + // Archived filter; when it is off they are folded back into the matching + // type-specific list so Public/Private show their archived rows too. + const visibleArchivedChannels = shouldHideArchivedChannels ? [] : archivedChannels; + const publicChannels = channels.concat(visibleArchivedChannels.filter((c) => c.type === Constants.OPEN_CHANNEL)); + const visiblePrivateChannels = privateChannels.concat(visibleArchivedChannels.filter((c) => c.type === Constants.PRIVATE_CHANNEL)); + + const allChannels = channels. + concat(privateChannels, extraDiscoverable). + concat(visibleArchivedChannels). + sort((a, b) => a.display_name.localeCompare(b.display_name)); const allChannelsWithoutJoined = this.getChannelsWithoutJoined(allChannels); - const publicChannelsWithoutJoined = this.getChannelsWithoutJoined(channels); + const publicChannelsWithoutJoined = this.getChannelsWithoutJoined(publicChannels); const archivedChannelsWithoutJoined = this.getChannelsWithoutJoined(archivedChannels); - const privateChannelsWithoutJoined = this.getChannelsWithoutJoined(privateChannels); + const privateChannelsWithoutJoined = this.getChannelsWithoutJoined(visiblePrivateChannels); const recommendedChannelsWithoutJoined = this.getChannelsWithoutJoined(recommendedChannels); // Channels the current user has pending requests against. The @@ -479,8 +506,8 @@ export default class BrowseChannels extends React.PureComponent { const filterOptions: Record = { [Filter.All]: shouldHideJoinedChannels ? allChannelsWithoutJoined : allChannels, [Filter.Archived]: shouldHideJoinedChannels ? archivedChannelsWithoutJoined : archivedChannels, - [Filter.Private]: shouldHideJoinedChannels ? privateChannelsWithoutJoined : privateChannels, - [Filter.Public]: shouldHideJoinedChannels ? publicChannelsWithoutJoined : channels, + [Filter.Private]: shouldHideJoinedChannels ? privateChannelsWithoutJoined : visiblePrivateChannels, + [Filter.Public]: shouldHideJoinedChannels ? publicChannelsWithoutJoined : publicChannels, [Filter.Recommended]: shouldHideJoinedChannels ? recommendedChannelsWithoutJoined : recommendedChannels, [Filter.Discoverable]: discoverableNonMember, [Filter.MyPendingRequests]: myPending, @@ -498,7 +525,7 @@ export default class BrowseChannels extends React.PureComponent { }; render() { - const {teamId, channelsRequestStarted, shouldHideJoinedChannels} = this.props; + const {teamId, channelsRequestStarted, shouldHideJoinedChannels, shouldHideArchivedChannels} = this.props; const {search, serverError: serverErrorState, searching} = this.state; this.activeChannels = this.getActiveChannels(); @@ -566,6 +593,8 @@ export default class BrowseChannels extends React.PureComponent { closeModal={this.props.actions.closeModal} hideJoinedChannelsPreference={this.handleShowJoinedChannelsPreference} rememberHideJoinedChannelsChecked={shouldHideJoinedChannels} + hideArchivedChannelsPreference={this.handleShowArchivedChannelsPreference} + rememberHideArchivedChannelsChecked={shouldHideArchivedChannels} channelsMemberCount={this.props.channelsMemberCount} /> {serverError} diff --git a/webapp/channels/src/components/browse_channels/index.ts b/webapp/channels/src/components/browse_channels/index.ts index 03cdbc62fe7..6492617f345 100644 --- a/webapp/channels/src/components/browse_channels/index.ts +++ b/webapp/channels/src/components/browse_channels/index.ts @@ -52,12 +52,18 @@ const getArchivedOtherChannels = createSelector( const getPrivateChannelsSelector = createSelector( 'getPrivateChannelsSelector', getChannelsInCurrentTeam, - (channels: Channel[]) => channels && channels.filter((c) => c.type === Constants.PRIVATE_CHANNEL), + + // Active private channels only. Archived private channels are surfaced + // exclusively through `archivedChannels`; including them here would both + // leak them past the Hide Archived toggle and double them up alongside the + // archived list. + (channels: Channel[]) => channels && channels.filter((c) => c.delete_at === 0 && c.type === Constants.PRIVATE_CHANNEL), ); function mapStateToProps(state: GlobalState) { const team = getCurrentTeam(state); const getGlobalItem = makeGetGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, 'false'); + const getHideArchivedItem = makeGetGlobalItem(StoragePrefixes.HIDE_ARCHIVED_CHANNELS, 'true'); return { channels: getChannelsWithoutArchived(state) || [], @@ -69,6 +75,7 @@ function mapStateToProps(state: GlobalState) { channelsRequestStarted: state.requests.channels.getChannels.status === RequestStatus.STARTED, myChannelMemberships: getMyChannelMemberships(state) || {}, shouldHideJoinedChannels: getGlobalItem(state) === 'true', + shouldHideArchivedChannels: getHideArchivedItem(state) === 'true', rhsState: getRhsState(state), rhsOpen: getIsRhsOpen(state), channelsMemberCount: getChannelsMemberCountSelector(state), diff --git a/webapp/channels/src/components/searchable_channel_list.test.tsx b/webapp/channels/src/components/searchable_channel_list.test.tsx index 4e300c92bae..a9604d97df4 100644 --- a/webapp/channels/src/components/searchable_channel_list.test.tsx +++ b/webapp/channels/src/components/searchable_channel_list.test.tsx @@ -13,7 +13,7 @@ import {compassIconForName} from 'components/channel_type_icon'; import {SearchableChannelList} from 'components/searchable_channel_list'; import {type MockIntl} from 'tests/helpers/intl-test-helper'; -import {renderWithContext, screen} from 'tests/react_testing_utils'; +import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils'; import {Filter} from './browse_channels/browse_channels'; @@ -48,10 +48,12 @@ describe('components/SearchableChannelList', () => { toggleArchivedChannels: jest.fn(), closeModal: jest.fn(), hideJoinedChannelsPreference: jest.fn(), + hideArchivedChannelsPreference: jest.fn(), changeFilter: jest.fn(), myChannelMemberships: {}, canShowArchivedChannels: false, rememberHideJoinedChannelsChecked: false, + rememberHideArchivedChannelsChecked: true, noResultsText: <>{'no channel found'}, filter: Filter.All, intl: { @@ -98,6 +100,48 @@ describe('components/SearchableChannelList', () => { expect(baseProps.search).toBeDefined(); }); + test('renders the Hide Archived checkbox reflecting the persisted preference', () => { + renderWithContext( + , + initialState, + ); + + const hideArchived = screen.getByLabelText('Hide archived channels'); + expect(hideArchived).toBeInTheDocument(); + expect(hideArchived).toHaveAttribute('aria-checked', 'true'); + }); + + test('does not render the Hide Archived checkbox when the Archived filter is active', () => { + renderWithContext( + , + initialState, + ); + + expect(screen.queryByLabelText('Hide archived channels')).not.toBeInTheDocument(); + }); + + test('clicking the Hide Archived checkbox toggles the preference', async () => { + const hideArchivedChannelsPreference = jest.fn(); + renderWithContext( + , + initialState, + ); + + const hideArchived = screen.getByLabelText('Hide archived channels'); + expect(hideArchived).toHaveAttribute('aria-checked', 'false'); + + await userEvent.click(hideArchived); + + expect(hideArchivedChannelsPreference).toHaveBeenCalledWith(true); + }); + test('should render ArchiveOutlineIcon for archived public channels', () => { const channels = [ { diff --git a/webapp/channels/src/components/searchable_channel_list.tsx b/webapp/channels/src/components/searchable_channel_list.tsx index b6c154e5d00..fd2b42fe45a 100644 --- a/webapp/channels/src/components/searchable_channel_list.tsx +++ b/webapp/channels/src/components/searchable_channel_list.tsx @@ -42,6 +42,8 @@ interface Props extends WrappedComponentProps { closeModal: (modalId: string) => void; hideJoinedChannelsPreference: (shouldHideJoinedChannels: boolean) => void; rememberHideJoinedChannelsChecked: boolean; + hideArchivedChannelsPreference: (shouldHideArchivedChannels: boolean) => void; + rememberHideArchivedChannelsChecked: boolean; loading?: boolean; channelsMemberCount?: Record; showRecommendedFilter?: boolean; @@ -451,6 +453,9 @@ export class SearchableChannelList extends React.PureComponent { this.props.hideJoinedChannelsPreference(true); } }; + handleArchivedChecked = () => { + this.props.hideArchivedChannelsPreference(!this.props.rememberHideArchivedChannelsChecked); + }; getEmptyStateMessage = () => { if (this.state.channelSearchValue.length > 0) { return ( @@ -851,6 +856,34 @@ export class SearchableChannelList extends React.PureComponent {
); + // The archived filter explicitly asks for archived channels, so hiding + // them there would leave an empty list — only offer the toggle elsewhere. + const hideArchivedButtonClass = classNames('get-app__checkbox', {checked: this.props.rememberHideArchivedChannelsChecked}); + const hideArchivedPreferenceCheckbox = this.props.filter === Filter.Archived ? null : ( +
{ + e.stopPropagation(); + if (e.key === 'Enter' || e.key === ' ') { + this.handleArchivedChecked(); + } + }} + role='checkbox' + aria-checked={this.props.rememberHideArchivedChannelsChecked} + aria-label={this.props.intl.formatMessage({id: 'more_channels.hide_archived_channels', defaultMessage: 'Hide archived channels'})} + tabIndex={0} + > +
+ {this.props.rememberHideArchivedChannelsChecked ? : null} +
+ +
+ ); + let channelCountLabel; if (channels.length === 0) { channelCountLabel = this.props.intl.formatMessage({id: 'more_channels.count_zero', defaultMessage: '0 Results'}); @@ -874,6 +907,7 @@ export class SearchableChannelList extends React.PureComponent {
{channelDropdown} + {hideArchivedPreferenceCheckbox} {hideJoinedPreferenceCheckbox}
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 3d6760a9ede..1ccdaa04cf3 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -6086,6 +6086,8 @@ "more_channels.create": "Create New Channel", "more_channels.discoverable": "Discoverable", "more_channels.discoverable.aria": "Discoverable private channel", + "more_channels.hide_archived": "Hide Archived", + "more_channels.hide_archived_channels": "Hide archived channels", "more_channels.hide_joined": "Hide Joined", "more_channels.hide_joined_channels": "Hide joined channels", "more_channels.joined": "Joined", diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index b4be69e3fe6..d47bbbeed15 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -834,6 +834,7 @@ export const StoragePrefixes = { INLINE_IMAGE_VISIBLE: 'isInlineImageVisible_', DELINQUENCY: 'delinquency_', HIDE_JOINED_CHANNELS: 'hideJoinedChannels', + HIDE_ARCHIVED_CHANNELS: 'hideArchivedChannels', HIDE_NOTIFICATION_PERMISSION_REQUEST_BANNER: 'hideNotificationPermissionRequestBanner', MARK_ALL_READ_WITHOUT_CONFIRM: 'mark_all_as_read_without_confirm', HAS_SEEN_FEATURE_TOAST: 'has_seen_feature_toast',