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
This commit is contained in:
Valentijn Nieman 2019-11-18 09:38:30 -05:00 committed by Saturnino Abril
parent fbf712dc8e
commit 6020df0b38
9 changed files with 200 additions and 31 deletions

View file

@ -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();

View file

@ -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 {
<View style={style.container}>
<View style={style.titleContainer}>
<Icon
name='globe'
name={this.props.isArchived ? 'archive' : 'globe'}
style={style.icon}
/>
<Text style={style.displayName}>

View file

@ -43,6 +43,34 @@ exports[`MoreChannels should match snapshot 1`] = `
value=""
/>
</View>
<View
style={
Array [
undefined,
null,
]
}
>
<Text
accessibilityRole="button"
onPress={[Function]}
style={
Object {
"fontWeight": "bold",
"marginBottom": 10,
"marginLeft": 10,
"marginTop": 20,
}
}
>
<Icon
allowFontScaling={false}
name="caret-down"
size={12}
/>
</Text>
</View>
<CustomList
data={
Array [

View file

@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {isLandscape} from 'app/selectors/device';
import {General} from 'mattermost-redux/constants';
import {getChannels, joinChannel, searchChannels} from 'mattermost-redux/actions/channels';
import {joinChannel, searchChannels} from 'mattermost-redux/actions/channels';
import {getChannelsInCurrentTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
@ -15,11 +15,13 @@ import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/channel';
import {handleSelectChannel, setChannelDisplayName, loadPublicAndArchivedChannels} from 'app/actions/views/channel';
import MoreChannels from './more_channels';
const joinableChannels = createSelector(
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
const joinablePublicChannels = createSelector(
getChannelsInCurrentTeam,
getMyChannelMemberships,
(channels, myMembers) => {
@ -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),

View file

@ -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 (
<ChannelListRow {...props}/>
<ChannelListRow
{...props}
isArchived={this.state.typeOfChannels === 'archived'}
/>
);
}
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 = (<Loading color={theme.centerChannelColor}/>);
@ -340,6 +391,31 @@ export default class MoreChannels extends PureComponent {
}),
};
let activeChannels = channels;
if (canShowArchivedChannels && typeOfChannels === 'archived') {
activeChannels = archivedChannels;
}
let channelDropdown;
if (canShowArchivedChannels) {
channelDropdown = (
<View style={[style.titleContainer, padding(isLandscape)]}>
<Text
accessibilityRole={'button'}
style={style.channelDropdown}
onPress={this.handleDropdownClick}
>
{typeOfChannels === 'public' ? publicChannelsText : archivedChannelsText}
{' '}
<Icon
name={'caret-down'}
/>
</Text>
</View>
);
}
content = (
<React.Fragment>
<View style={[style.searchBar, padding(isLandscape)]}>
@ -362,8 +438,9 @@ export default class MoreChannels extends PureComponent {
value={term}
/>
</View>
{channelDropdown}
<CustomList
data={channels}
data={activeChannels}
extraData={loading}
key='custom_list'
loading={loading}
@ -411,5 +488,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 26,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
channelDropdown: {
fontWeight: 'bold',
marginLeft: 10,
marginTop: 20,
marginBottom: 10,
},
};
});

View file

@ -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(
<MoreChannels {...baseProps}/>,
{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);
});
});

View file

@ -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...",

4
package-lock.json generated
View file

@ -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",

View file

@ -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",