diff --git a/app/actions/views/channel_members.js b/app/actions/views/channel_members.js new file mode 100644 index 000000000..74f2dad8d --- /dev/null +++ b/app/actions/views/channel_members.js @@ -0,0 +1,16 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {removeChannelMember} from 'service/actions/channels'; + +export function handleRemoveChannelMembers(teamId, channelId, members) { + return async (dispatch, getState) => { + try { + const requests = members.map((m) => dispatch(removeChannelMember(teamId, channelId, m, getState))); + + await Promise.all(requests); + } catch (error) { + // should be handled by global error handling + } + }; +} diff --git a/app/scenes/channel_add_members/channel_add_members.js b/app/scenes/channel_add_members/channel_add_members.js index 8211fbc9a..49bd0da3f 100644 --- a/app/scenes/channel_add_members/channel_add_members.js +++ b/app/scenes/channel_add_members/channel_add_members.js @@ -102,7 +102,6 @@ export default class ChannelAddMembers extends PureComponent { loadingMembers={this.props.loadMoreRequestStatus === 'started'} selectable={true} onRowSelect={this.handleRowSelect} - showSections={true} /> ); diff --git a/app/scenes/channel_info/channel_info.js b/app/scenes/channel_info/channel_info.js index 392270c53..0007a764b 100644 --- a/app/scenes/channel_info/channel_info.js +++ b/app/scenes/channel_info/channel_info.js @@ -43,6 +43,7 @@ class ChannelInfo extends PureComponent { currentChannelMemberCount: PropTypes.number, isFavorite: PropTypes.bool.isRequired, leaveChannelRequest: PropTypes.object.isRequired, + isAdmin: PropTypes.bool.isRequired, actions: PropTypes.shape({ getChannelStats: PropTypes.func.isRequired, goToChannelMembers: PropTypes.func.isRequired, @@ -129,7 +130,8 @@ class ChannelInfo extends PureComponent { const { currentChannel, currentChannelCreatorName, - currentChannelMemberCount + currentChannelMemberCount, + isAdmin } = this.props; return ( @@ -166,23 +168,27 @@ class ChannelInfo extends PureComponent { - - - - + {isAdmin && + + + + + + + } this.handleLeave()} defaultMessage='Leave Channel' diff --git a/app/scenes/channel_info/channel_info_container.js b/app/scenes/channel_info/channel_info_container.js index b2eb5b9e4..1697a6d8d 100644 --- a/app/scenes/channel_info/channel_info_container.js +++ b/app/scenes/channel_info/channel_info_container.js @@ -9,7 +9,8 @@ import {goToChannelMembers, goToChannelAddMembers, goBack} from 'app/actions/nav import {getChannelStats} from 'service/actions/channels'; import {markFavorite, unmarkFavorite, leaveChannel} from 'app/actions/views/channel'; import {getCurrentChannel, getCurrentChannelStats, getChannelsByCategory} from 'service/selectors/entities/channels'; -import {getUser} from 'service/selectors/entities/users'; +import {getTheme} from 'service/selectors/entities/preferences'; +import {getUser, getCurrentUserRoles} from 'service/selectors/entities/users'; import ChannelInfo from './channel_info'; @@ -21,6 +22,9 @@ function mapStateToProps(state, ownProps) { const favoriteChannels = getChannelsByCategory(state).favoriteChannels.map((f) => f.id); const isFavorite = favoriteChannels.indexOf(currentChannel.id) > -1; const leaveChannelRequest = state.requests.channels.leaveChannel; + const currentUserRoles = getCurrentUserRoles(state); + + const isAdmin = currentUserRoles.includes('_admin'); return { ...ownProps, @@ -28,7 +32,9 @@ function mapStateToProps(state, ownProps) { currentChannelCreatorName, currentChannelMemberCount, isFavorite, - leaveChannelRequest + leaveChannelRequest, + theme: getTheme(state), + isAdmin }; } diff --git a/app/scenes/channel_members/channel_members.js b/app/scenes/channel_members/channel_members.js index 47d53fb20..671915669 100644 --- a/app/scenes/channel_members/channel_members.js +++ b/app/scenes/channel_members/channel_members.js @@ -3,44 +3,137 @@ import React, {PropTypes, PureComponent} from 'react'; import { + Alert, InteractionManager, StyleSheet, View } from 'react-native'; +import {injectIntl, intlShape} from 'react-intl'; import MemberList from 'app/components/member_list'; +import ChannelMembersTitle from './channel_members_title'; +import RemoveMemberButton from './remove_member_button'; + const style = StyleSheet.create({ container: { flex: 1 } }); -export default class ChannelMembers extends PureComponent { +class ChannelMembers extends PureComponent { static propTypes = { + intl: intlShape.isRequired, currentChannel: PropTypes.object, currentChannelMembers: PropTypes.array.isRequired, currentChannelMemberCount: PropTypes.number.isRequired, currentTeam: PropTypes.object, preferences: PropTypes.object, requestStatus: PropTypes.string, + isAdmin: PropTypes.bool.isRequired, + subscribeToHeaderEvent: React.PropTypes.func, + unsubscribeFromHeaderEvent: React.PropTypes.func, actions: PropTypes.shape({ - getProfilesInChannel: PropTypes.func.isRequired + getProfilesInChannel: PropTypes.func.isRequired, + goBack: PropTypes.func.isRequired, + handleRemoveChannelMembers: PropTypes.func.isRequired }) } + static navigationProps = { + renderTitleComponent: () => { + return ; + }, + renderRightComponent: (props, emitter) => { + return ; + } + } + + state = { + selectedMembers: {} + } + + componentWillMount() { + this.props.subscribeToHeaderEvent('remove_members', this.handleRemoveMembersPress); + } + componentDidMount() { InteractionManager.runAfterInteractions(() => { this.props.actions.getProfilesInChannel(this.props.currentTeam.id, this.props.currentChannel.id, 0); }); } + componentWillUnmount() { + this.props.unsubscribeFromHeaderEvent('remove_members'); + } + + handleRemoveMembersPress = () => { + const {selectedMembers} = this.state; + const membersToRemove = Object.keys(selectedMembers).filter((m) => selectedMembers[m]); + + const {formatMessage} = this.props.intl; + if (!membersToRemove.length) { + Alert.alert( + formatMessage({ + id: 'mobile.routes.channel_members.action', + defaultMessage: '{term} Members' + }, { + term: 'Remove' + }), + formatMessage({ + id: 'mobile.routes.channel_members.action_message', + defaultMessage: 'You must select at least one member to {term} {prep} the channel.' + }, { + term: 'remove', + prep: 'from' + }) + ); + return; + } + + Alert.alert( + formatMessage({ + id: 'mobile.routes.channel_members.action', + defaultMessage: '{term} Members' + }, { + term: 'Remove' + }), + formatMessage({ + id: 'mobile.routes.channel_members.action_message_confirm', + defaultMessage: 'Are you sure you want to {term} the selected members {prep} the channel?' + }, { + term: 'remove', + prep: 'from' + }), + [{ + text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'}) + }, { + text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}), + onPress: () => this.removeMembers(membersToRemove) + }] + ); + } + + removeMembers = (membersToRemove) => { + const {actions, currentTeam, currentChannel} = this.props; + actions.handleRemoveChannelMembers(currentTeam.id, currentChannel.id, membersToRemove).then(() => { + actions.goBack(); + }); + } + loadMoreMembers = () => { if (this.props.requestStatus !== 'started' && this.props.currentChannelMembers.length < this.props.currentChannelMemberCount) { this.props.actions.getProfilesInChannel(this.props.currentTeam.id, this.props.currentChannel.id, this.props.currentChannelMembers.length); } } + handleRowSelect = (id) => { + const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]}); + this.setState({ + selectedMembers + }); + } + render() { return ( @@ -49,8 +142,12 @@ export default class ChannelMembers extends PureComponent { onListEndReached={this.loadMoreMembers} preferences={this.props.preferences} loadingMembers={this.props.requestStatus === 'started'} + selectable={this.props.isAdmin} + onRowSelect={this.handleRowSelect} /> ); } } + +export default injectIntl(ChannelMembers); diff --git a/app/scenes/channel_members/channel_members_container.js b/app/scenes/channel_members/channel_members_container.js index 07245cf23..2880aeca2 100644 --- a/app/scenes/channel_members/channel_members_container.js +++ b/app/scenes/channel_members/channel_members_container.js @@ -5,16 +5,21 @@ import {bindActionCreators} from 'redux'; import navigationSceneConnect from '../navigationSceneConnect'; +import {goBack} from 'app/actions/navigation'; +import {handleRemoveChannelMembers} from 'app/actions/views/channel_members'; import {getCurrentChannel, getCurrentChannelStats} from 'service/selectors/entities/channels'; import {getMyPreferences} from 'service/selectors/entities/preferences'; import {getCurrentTeam} from 'service/selectors/entities/teams'; -import {getProfilesInCurrentChannel} from 'service/selectors/entities/users'; +import {getProfilesInCurrentChannel, getCurrentUserRoles} from 'service/selectors/entities/users'; import {getProfilesInChannel} from 'service/actions/users'; import ChannelMembers from './channel_members'; function mapStateToProps(state) { const currentChannelMemberCount = getCurrentChannelStats(state) && getCurrentChannelStats(state).member_count; + const currentUserRoles = getCurrentUserRoles(state); + + const isAdmin = currentUserRoles.includes('_admin'); return { currentChannel: getCurrentChannel(state), @@ -22,14 +27,17 @@ function mapStateToProps(state) { currentChannelMemberCount, currentTeam: getCurrentTeam(state), preferences: getMyPreferences(state), - requestStatus: state.requests.users.getProfilesInChannel.status + requestStatus: state.requests.users.getProfilesInChannel.status, + isAdmin }; } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - getProfilesInChannel + getProfilesInChannel, + goBack, + handleRemoveChannelMembers }, dispatch) }; } diff --git a/app/scenes/channel_members/channel_members_title.js b/app/scenes/channel_members/channel_members_title.js new file mode 100644 index 000000000..6ee665d02 --- /dev/null +++ b/app/scenes/channel_members/channel_members_title.js @@ -0,0 +1,47 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PropTypes} from 'react'; +import {connect} from 'react-redux'; +import { + View +} from 'react-native'; + +import {getCurrentUserRoles} from 'service/selectors/entities/users'; +import {getTheme} from 'service/selectors/entities/preferences'; + +import FormattedText from 'app/components/formatted_text'; + +function ChannelMemberTitle(props) { + return ( + + + + ); +} + +ChannelMemberTitle.propTypes = { + isAdmin: PropTypes.bool, + theme: PropTypes.object +}; + +ChannelMemberTitle.defaultProps = { + theme: {} +}; + +function mapStateToProps(state) { + const currentUserRoles = getCurrentUserRoles(state); + + const isAdmin = currentUserRoles.includes('_admin'); + + return { + isAdmin, + theme: getTheme(state) + }; +} + +export default connect(mapStateToProps)(ChannelMemberTitle); diff --git a/app/scenes/channel_members/remove_member_button.js b/app/scenes/channel_members/remove_member_button.js new file mode 100644 index 000000000..e372c0604 --- /dev/null +++ b/app/scenes/channel_members/remove_member_button.js @@ -0,0 +1,57 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PropTypes} from 'react'; +import {connect} from 'react-redux'; +import { + TouchableOpacity, + View +} from 'react-native'; + +import FormattedText from 'app/components/formatted_text'; + +import {getCurrentUserRoles} from 'service/selectors/entities/users'; +import {getTheme} from 'service/selectors/entities/preferences'; + +function RemoveMemberButton(props) { + if (!props.isAdmin) { + return null; + } + + return ( + + props.emitter('remove_members')} + style={{paddingHorizontal: 15}} + > + + + + ); +} + +RemoveMemberButton.propTypes = { + emitter: PropTypes.func.isRequired, + isAdmin: PropTypes.bool.isRequired, + theme: PropTypes.object +}; + +RemoveMemberButton.defaultProps = { + theme: {} +}; + +function mapStateToProps(state) { + const currentUserRoles = getCurrentUserRoles(state); + + const isAdmin = currentUserRoles.includes('_admin'); + return { + isAdmin, + theme: getTheme(state) + }; +} + +export default connect(mapStateToProps)(RemoveMemberButton); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 574ac8bd2..a2b861b95 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1512,6 +1512,9 @@ "mobile.routes.channelInfo.createdBy": "Created by {creator} on ", "mobile.routes.channelInfo.delete_channel": "Delete Channel", "mobile.routes.channelInfo.favorite": "Favorite", + "mobile.routes.channel_members.action": "{term} Members", + "mobile.routes.channel_members.action_message": "You must select at least one member to {term} {prep} the channel.", + "mobile.routes.channel_members.action_message_confirm": "Are you sure you want to {term} the selected members {prep} the channel?", "more_channels.close": "Close", "more_channels.create": "Create New Channel", "more_channels.createClick": "Click 'Create New Channel' to make a new one", diff --git a/service/selectors/entities/channels.js b/service/selectors/entities/channels.js index 0fcaae9b2..ce6f9053d 100644 --- a/service/selectors/entities/channels.js +++ b/service/selectors/entities/channels.js @@ -17,6 +17,10 @@ export function getCurrentChannelId(state) { return state.entities.channels.currentId; } +export function getChannelMemberships(state) { + return state.entities.channels.myMembers; +} + export const getCurrentChannel = createSelector( getAllChannels, getCurrentChannelId, @@ -31,6 +35,14 @@ export const getCurrentChannel = createSelector( } ); +export const getCurrentChannelMembership = createSelector( + getCurrentChannelId, + getChannelMemberships, + (currentChannelId, channelMemberships) => { + return channelMemberships[currentChannelId]; + } +); + export const getCurrentChannelStats = createSelector( getAllChannelStats, getCurrentChannelId, diff --git a/service/selectors/entities/teams.js b/service/selectors/entities/teams.js index 8441573d4..039898053 100644 --- a/service/selectors/entities/teams.js +++ b/service/selectors/entities/teams.js @@ -17,6 +17,10 @@ export function getTeamStats(state) { return state.entities.teams.stats; } +export function getTeamMemberships(state) { + return state.entities.teams.myMembers; +} + export const getCurrentTeam = createSelector( getTeams, getCurrentTeamId, @@ -25,6 +29,14 @@ export const getCurrentTeam = createSelector( } ); +export const getCurrentTeamMembership = createSelector( + getCurrentTeamId, + getTeamMemberships, + (currentTeamId, teamMemberships) => { + return teamMemberships[currentTeamId]; + } +); + export const getCurrentTeamUrl = createSelector( getCurrentUrl, getCurrentTeam, diff --git a/service/selectors/entities/users.js b/service/selectors/entities/users.js index 0ef49b6cd..efdb6d80c 100644 --- a/service/selectors/entities/users.js +++ b/service/selectors/entities/users.js @@ -3,7 +3,8 @@ import {createSelector} from 'reselect'; -import {getCurrentChannelId} from './channels'; +import {getCurrentChannelId, getCurrentChannelMembership} from './channels'; +import {getCurrentTeamMembership} from './teams'; export function getCurrentUserId(state) { return state.entities.users.currentId; @@ -37,6 +38,15 @@ export const getCurrentUser = createSelector( } ); +export const getCurrentUserRoles = createSelector( + getCurrentChannelMembership, + getCurrentTeamMembership, + getCurrentUser, + (currentChannelMembership, currentTeamMembership, currentUser) => { + return `${currentTeamMembership.roles} ${currentChannelMembership.roles} ${currentUser.roles}`; + } +); + export const getProfileSetInCurrentChannel = createSelector( getCurrentChannelId, getProfilesInChannel,