From 6020df0b381ceeeedc4c994634018eeb8bea161c Mon Sep 17 00:00:00 2001 From: Valentijn Nieman Date: Mon, 18 Nov 2019 09:38:30 -0500 Subject: [PATCH] MM-19337 Enable users to view archived channels (#3514) * Archived channels dropdown in more channels modal * Rename redux actions for archived channels * Fixed tests and updated snapshots * Unit test for search in more_channels * Use translation for dropdown label * Minimum server requirement for dropdown * Use BottomSheet instead of Picker component * loadPublicAndArchivedChannels action instead of separate channel get actions * Add styles to StyleSheet * Update mattermost-redux hash * Update mattermost-redux hash * Default case for switching channels dropdown * Improve imports in more_channels.js component * Fix typo in import * Add padding to dropdown if landscape * Update snapshot * Page counter for public and archive channels * Updated mattermost-redux commit hash * Bottom sheet title for ios * i18n-extract for new showArchived and showPublic strings * Update mattermost-redux commit hash to latest master --- app/actions/views/channel.js | 22 +++ .../channel_list_row/channel_list_row.js | 3 +- .../__snapshots__/more_channels.test.js.snap | 28 ++++ app/screens/more_channels/index.js | 22 ++- app/screens/more_channels/more_channels.js | 125 +++++++++++++++--- .../more_channels/more_channels.test.js | 20 ++- assets/base/i18n/en.json | 5 + package-lock.json | 4 +- package.json | 2 +- 9 files changed, 200 insertions(+), 31 deletions(-) diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 4d89dcd77..fed9f0c7d 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -14,6 +14,8 @@ import { leaveChannel as serviceLeaveChannel, selectChannel, getChannelStats, + getChannels, + getArchivedChannels, } from 'mattermost-redux/actions/channels'; import { getPosts, @@ -73,6 +75,26 @@ export function loadChannelsByTeamName(teamName) { }; } +export function loadPublicAndArchivedChannels(teamId, publicPage, archivedPage, perPage, shouldLoadArchivedChannels) { + return async (dispatch) => { + return dispatch(getChannels( + teamId, + publicPage, + perPage + )).then(async (publicChannels) => { + if (shouldLoadArchivedChannels) { + const archivedChannels = await dispatch(getArchivedChannels( + teamId, + archivedPage, + perPage + )); + return archivedChannels; + } + return publicChannels; + }); + }; +} + export function loadProfilesAndTeamMembersForDMSidebar(teamId) { return async (dispatch, getState) => { const state = getState(); diff --git a/app/components/custom_list/channel_list_row/channel_list_row.js b/app/components/custom_list/channel_list_row/channel_list_row.js index 01feaf7a0..096339ae2 100644 --- a/app/components/custom_list/channel_list_row/channel_list_row.js +++ b/app/components/custom_list/channel_list_row/channel_list_row.js @@ -15,6 +15,7 @@ import CustomListRow from 'app/components/custom_list/custom_list_row'; export default class ChannelListRow extends React.PureComponent { static propTypes = { id: PropTypes.string.isRequired, + isArchived: PropTypes.bool, theme: PropTypes.object.isRequired, channel: PropTypes.object.isRequired, ...CustomListRow.propTypes, @@ -53,7 +54,7 @@ export default class ChannelListRow extends React.PureComponent { diff --git a/app/screens/more_channels/__snapshots__/more_channels.test.js.snap b/app/screens/more_channels/__snapshots__/more_channels.test.js.snap index a0921f6cd..ef31e17bd 100644 --- a/app/screens/more_channels/__snapshots__/more_channels.test.js.snap +++ b/app/screens/more_channels/__snapshots__/more_channels.test.js.snap @@ -43,6 +43,34 @@ exports[`MoreChannels should match snapshot 1`] = ` value="" /> + + + + + + { @@ -29,11 +31,19 @@ const joinableChannels = createSelector( } ); +const teamArchivedChannels = createSelector( + getChannelsInCurrentTeam, + (channels) => { + return channels.filter((c) => c.delete_at !== 0); + } +); + function mapStateToProps(state) { const config = getConfig(state); const license = getLicense(state); const roles = getCurrentUserRoles(state); - const channels = joinableChannels(state); + const channels = joinablePublicChannels(state); + const archivedChannels = teamArchivedChannels(state); const currentTeamId = getCurrentTeamId(state); return { @@ -41,8 +51,10 @@ function mapStateToProps(state) { currentUserId: getCurrentUserId(state), currentTeamId, channels, + archivedChannels, theme: getTheme(state), isLandscape: isLandscape(state), + canShowArchivedChannels: isMinimumServerVersion(state.entities.general.serverVersion, 5, 18), }; } @@ -51,7 +63,7 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ handleSelectChannel, joinChannel, - getChannels, + loadPublicAndArchivedChannels, searchChannels, setChannelDisplayName, }, dispatch), diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js index de0cfbbbd..7f1c865e6 100644 --- a/app/screens/more_channels/more_channels.js +++ b/app/screens/more_channels/more_channels.js @@ -4,7 +4,8 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; -import {Platform, View} from 'react-native'; +import {Platform, View, Text} from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; import {Navigation} from 'react-native-navigation'; import {debounce} from 'mattermost-redux/actions/helpers'; @@ -12,6 +13,7 @@ import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; +import BottomSheet from 'app/utils/bottom_sheet'; import CustomList from 'app/components/custom_list'; import ChannelListRow from 'app/components/custom_list/channel_list_row'; import FormattedText from 'app/components/formatted_text'; @@ -33,18 +35,20 @@ export default class MoreChannels extends PureComponent { actions: PropTypes.shape({ handleSelectChannel: PropTypes.func.isRequired, joinChannel: PropTypes.func.isRequired, - getChannels: PropTypes.func.isRequired, + loadPublicAndArchivedChannels: PropTypes.func.isRequired, searchChannels: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, canCreateChannels: PropTypes.bool.isRequired, channels: PropTypes.array, + archivedChannels: PropTypes.array, closeButton: PropTypes.object, currentUserId: PropTypes.string.isRequired, currentTeamId: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, isLandscape: PropTypes.bool.isRequired, + canShowArchivedChannels: PropTypes.bool.isRequired, }; static defaultProps = { @@ -59,12 +63,15 @@ export default class MoreChannels extends PureComponent { super(props, context); this.searchTimeoutId = 0; - this.page = -1; + this.publicPage = -1; + this.archivedPage = -1; this.next = true; this.mounted = false; this.state = { channels: props.channels.slice(0, General.CHANNELS_CHUNK_SIZE), + archivedChannels: props.archivedChannels.slice(0, General.CHANNELS_CHUNK_SIZE), + typeOfChannels: 'public', loading: false, adding: false, term: '', @@ -130,10 +137,11 @@ export default class MoreChannels extends PureComponent { } cancelSearch = () => { - const {channels} = this.props; + const {channels, archivedChannels} = this.props; this.setState({ channels, + archivedChannels, term: '', }); }; @@ -143,15 +151,17 @@ export default class MoreChannels extends PureComponent { }; doGetChannels = () => { - const {actions, currentTeamId} = this.props; + const {actions, currentTeamId, canShowArchivedChannels} = this.props; const {loading, term} = this.state; if (this.next && !loading && !term && this.mounted) { this.setState({loading: true}, () => { - actions.getChannels( + actions.loadPublicAndArchivedChannels( currentTeamId, - this.page + 1, - General.CHANNELS_CHUNK_SIZE + this.publicPage + 1, + this.archivedPage + 1, + General.CHANNELS_CHUNK_SIZE, + canShowArchivedChannels, ).then(this.loadedChannels); }); } @@ -295,36 +305,77 @@ export default class MoreChannels extends PureComponent { renderItem = (props) => { return ( - + ); } searchChannels = (text) => { - const {actions, channels, currentTeamId} = this.props; + const {actions, channels, archivedChannels, currentTeamId, canShowArchivedChannels} = this.props; + const {typeOfChannels} = this.state; if (text) { - const filtered = this.filterChannels(channels, text); - this.setState({ - channels: filtered, - term: text, - }); - clearTimeout(this.searchTimeoutId); - + if (typeOfChannels === 'public') { + const filtered = this.filterChannels(channels, text); + this.setState({ + channels: filtered, + term: text, + }); + clearTimeout(this.searchTimeoutId); + } else if (typeOfChannels === 'archived' && canShowArchivedChannels) { + const filtered = this.filterChannels(archivedChannels, text); + this.setState({ + archivedChannels: filtered, + term: text, + }); + clearTimeout(this.searchTimeoutId); + } this.searchTimeoutId = setTimeout(() => { - actions.searchChannels(currentTeamId, text.toLowerCase()); + actions.searchChannels(currentTeamId, text.toLowerCase(), typeOfChannels === 'archived'); }, General.SEARCH_TIMEOUT_MILLISECONDS); } else { this.cancelSearch(); } }; + handleDropdownClick = () => { + const {formatMessage} = this.context.intl; + const publicChannelsText = formatMessage({id: 'more_channels.publicChannels', defaultMessage: 'Public Channels'}); + const archivedChannelsText = formatMessage({id: 'more_channels.archivedChannels', defaultMessage: 'Archived Channels'}); + const titleText = formatMessage({id: 'more_channels.dropdownTitle', defaultMessage: 'Show'}); + const cancelText = 'Cancel'; + BottomSheet.showBottomSheetWithOptions({ + options: [publicChannelsText, archivedChannelsText, cancelText], + cancelButtonIndex: 2, + title: titleText, + }, (value) => { + let typeOfChannels; + switch (value) { + case 0: + typeOfChannels = 'public'; + break; + case 1: + typeOfChannels = 'archived'; + break; + default: + typeOfChannels = this.state.typeOfChannels; + } + this.setState({typeOfChannels}); + }); + } + render() { const {formatMessage} = this.context.intl; - const {theme, isLandscape} = this.props; - const {adding, channels, loading, term} = this.state; + const {theme, isLandscape, canShowArchivedChannels} = this.props; + const {adding, channels, archivedChannels, loading, term, typeOfChannels} = this.state; const more = term ? () => true : this.getChannels; const style = getStyleFromTheme(theme); + const publicChannelsText = formatMessage({id: 'more_channels.showPublicChannels', defaultMessage: 'Show: Public Channels'}); + const archivedChannelsText = formatMessage({id: 'more_channels.showArchivedChannels', defaultMessage: 'Show: Archived Channels'}); + let content; if (adding) { content = (); @@ -340,6 +391,31 @@ export default class MoreChannels extends PureComponent { }), }; + let activeChannels = channels; + + if (canShowArchivedChannels && typeOfChannels === 'archived') { + activeChannels = archivedChannels; + } + + let channelDropdown; + if (canShowArchivedChannels) { + channelDropdown = ( + + + {typeOfChannels === 'public' ? publicChannelsText : archivedChannelsText} + {' '} + + + + ); + } + content = ( @@ -362,8 +438,9 @@ export default class MoreChannels extends PureComponent { value={term} /> + {channelDropdown} { fontSize: 26, color: changeOpacity(theme.centerChannelColor, 0.5), }, + channelDropdown: { + fontWeight: 'bold', + marginLeft: 10, + marginTop: 20, + marginBottom: 10, + }, }; }); diff --git a/app/screens/more_channels/more_channels.test.js b/app/screens/more_channels/more_channels.test.js index c2ebd9628..300735e54 100644 --- a/app/screens/more_channels/more_channels.test.js +++ b/app/screens/more_channels/more_channels.test.js @@ -16,7 +16,7 @@ describe('MoreChannels', () => { const actions = { handleSelectChannel: jest.fn(), joinChannel: jest.fn(), - getChannels: jest.fn().mockResolvedValue({data: [{id: 'id2', name: 'name2', display_name: 'display_name2'}]}), + loadPublicAndArchivedChannels: jest.fn().mockResolvedValue({data: [{id: 'id2', name: 'name2', display_name: 'display_name2'}]}), searchChannels: jest.fn(), setChannelDisplayName: jest.fn(), }; @@ -25,12 +25,14 @@ describe('MoreChannels', () => { actions, canCreateChannels: true, channels: [{id: 'id', name: 'name', display_name: 'display_name'}], + archivedChannels: [{id: 'id2', name: 'archived', display_name: 'archived channel'}], closeButton: {}, currentUserId: 'current_user_id', currentTeamId: 'current_team_id', theme: Preferences.THEMES.default, componentId: 'component-id', isLandscape: false, + canShowArchivedChannels: true, }; test('should match snapshot', () => { @@ -91,4 +93,20 @@ describe('MoreChannels', () => { expect(wrapper.state('term')).toEqual(''); expect(wrapper.state('channels')).toEqual(baseProps.channels); }); + + test('should search correct channels', () => { + const wrapper = shallow( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + const instance = wrapper.instance(); + + wrapper.setState({typeOfChannels: 'public'}); + instance.searchChannels('display_name'); + expect(wrapper.state('channels')).toEqual(baseProps.channels); + + wrapper.setState({typeOfChannels: 'archived'}); + instance.searchChannels('archived channel'); + expect(wrapper.state('archivedChannels')).toEqual(baseProps.archivedChannels); + }); }); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 061d6bc87..c75414a53 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -492,7 +492,12 @@ "modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to \"Do Not Disturb\" and disable Automatic Replies?", "modal.manual_status.auto_responder.message_offline": "Would you like to switch your status to \"Offline\" and disable Automatic Replies?", "modal.manual_status.auto_responder.message_online": "Would you like to switch your status to \"Online\" and disable Automatic Replies?", + "more_channels.archivedChannels": "Archived Channels", + "more_channels.dropdownTitle": "Show", "more_channels.noMore": "No more channels to join", + "more_channels.publicChannels": "Public Channels", + "more_channels.showArchivedChannels": "Show: Archived Channels", + "more_channels.showPublicChannels": "Show: Public Channels", "more_channels.title": "More Channels", "msg_typing.areTyping": "{users} and {last} are typing...", "msg_typing.isTyping": "{user} is typing...", diff --git a/package-lock.json b/package-lock.json index 9fe3dd749..68639aecd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7816,8 +7816,8 @@ } }, "mattermost-redux": { - "version": "github:mattermost/mattermost-redux#98a375cea54f78fb6c0ede01ee4e9aec009f9fa5", - "from": "github:mattermost/mattermost-redux#98a375cea54f78fb6c0ede01ee4e9aec009f9fa5", + "version": "github:mattermost/mattermost-redux#ebd4fd361e982bf2f9b5737642dc5fcf6e1fa3f7", + "from": "github:mattermost/mattermost-redux#ebd4fd361e982bf2f9b5737642dc5fcf6e1fa3f7", "requires": { "form-data": "2.5.1", "gfycat-sdk": "1.4.18", diff --git a/package.json b/package.json index e9e0f22c4..19e8001e0 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "intl": "1.2.5", "jail-monkey": "2.3.0", "jsc-android": "241213.1.0", - "mattermost-redux": "github:mattermost/mattermost-redux#98a375cea54f78fb6c0ede01ee4e9aec009f9fa5", + "mattermost-redux": "github:mattermost/mattermost-redux#ebd4fd361e982bf2f9b5737642dc5fcf6e1fa3f7", "mime-db": "1.42.0", "moment-timezone": "0.5.27", "prop-types": "15.7.2",