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,