diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 68b171855..36433392c 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -3993,6 +3993,536 @@ Array [
]
`;
+exports[`Storyshots List alert 1`] = `
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`Storyshots List header 1`] = `
-
- Press me
-
+
+ Press me
+
+
-
- I'm disabled
-
+
+ I'm disabled
+
+
-
- Chats
-
+
+ Chats
+
+
@@ -4440,25 +5000,35 @@ exports[`Storyshots List title and subtitle 1`] = `
}
}
>
-
- Chats
-
+
+ Chats
+
+
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
-
- 0
-
+
+ 0
+
+
@@ -4796,25 +5386,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 1
-
+
+ 1
+
+
@@ -4868,25 +5468,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 2
-
+
+ 2
+
+
@@ -4940,25 +5550,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 3
-
+
+ 3
+
+
@@ -5012,25 +5632,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 4
-
+
+ 4
+
+
@@ -5084,25 +5714,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 5
-
+
+ 5
+
+
@@ -5156,25 +5796,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 6
-
+
+ 6
+
+
@@ -5228,25 +5878,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 7
-
+
+ 7
+
+
@@ -5300,25 +5960,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 8
-
+
+ 8
+
+
@@ -5372,25 +6042,35 @@ exports[`Storyshots List with FlatList 1`] = `
}
}
>
-
- 9
-
+
+ 9
+
+
@@ -5586,25 +6266,35 @@ exports[`Storyshots List with bigger font 1`] = `
}
}
>
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
@@ -7217,25 +7987,35 @@ exports[`Storyshots List with custom colors 1`] = `
}
}
>
-
- Press me!
-
+
+ Press me!
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Icon Left
-
+
+ Icon Left
+
+
@@ -8253,25 +9083,35 @@ exports[`Storyshots List with icon 1`] = `
}
}
>
-
- Icon Right
-
+
+ Icon Right
+
+
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
-
- Show Action Indicator
-
+
+ Show Action Indicator
+
+
-
- Section Item
-
+
+ Section Item
+
+
@@ -8760,25 +9630,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -8848,25 +9728,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -8915,25 +9805,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9031,25 +9931,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9098,25 +10008,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9241,25 +10161,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9308,25 +10238,35 @@ exports[`Storyshots List with section and info 1`] = `
}
}
>
-
- Section Item
-
+
+ Section Item
+
+
@@ -9525,25 +10465,35 @@ exports[`Storyshots List with small font 1`] = `
}
}
>
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
-
- Chats
-
+
+ Chats
+
+
));
diff --git a/app/containers/List/ListItem.js b/app/containers/List/ListItem.js
index 6ce7bb6fc..aa3ecbdf0 100644
--- a/app/containers/List/ListItem.js
+++ b/app/containers/List/ListItem.js
@@ -10,8 +10,9 @@ import sharedStyles from '../../views/Styles';
import { withTheme } from '../../theme';
import I18n from '../../i18n';
import { Icon } from '.';
-import { BASE_HEIGHT, PADDING_HORIZONTAL } from './constants';
+import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
import { withDimensions } from '../../dimensions';
+import { CustomIcon } from '../../lib/Icons';
const styles = StyleSheet.create({
container: {
@@ -34,7 +35,15 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'center'
},
+ textAlertContainer: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ alertIcon: {
+ paddingLeft: 4
+ },
title: {
+ flexShrink: 1,
fontSize: 16,
...sharedStyles.textRegular
},
@@ -50,7 +59,7 @@ const styles = StyleSheet.create({
});
const Content = React.memo(({
- title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale
+ title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale, alert
}) => (
{left
@@ -61,7 +70,12 @@ const Content = React.memo(({
)
: null}
- {translateTitle ? I18n.t(title) : title}
+
+ {translateTitle ? I18n.t(title) : title}
+ {alert ? (
+
+ ) : null}
+
{subtitle
? {translateSubtitle ? I18n.t(subtitle) : subtitle}
: null
@@ -123,7 +137,8 @@ Content.propTypes = {
translateTitle: PropTypes.bool,
translateSubtitle: PropTypes.bool,
showActionIndicator: PropTypes.bool,
- fontScale: PropTypes.number
+ fontScale: PropTypes.number,
+ alert: PropTypes.bool
};
Content.defaultProps = {
diff --git a/app/containers/List/constants.js b/app/containers/List/constants.js
index b69a04f95..8144096d3 100644
--- a/app/containers/List/constants.js
+++ b/app/containers/List/constants.js
@@ -1,2 +1,3 @@
export const PADDING_HORIZONTAL = 12;
export const BASE_HEIGHT = 46;
+export const ICON_SIZE = 20;
diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json
index 8cc6cb278..2e8391956 100644
--- a/app/i18n/locales/en.json
+++ b/app/i18n/locales/en.json
@@ -290,6 +290,7 @@
"last_message": "last message",
"Leave_channel": "Leave channel",
"leaving_room": "leaving room",
+ "Leave": "Leave",
"leave": "leave",
"Legal": "Legal",
"Light": "Light",
@@ -726,5 +727,13 @@
"Auto-join": "Auto-join",
"Delete_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace",
"Confirmation": "Confirmation",
- "invalid-room": "Invalid room"
+ "invalid-room": "Invalid room",
+ "You_are_leaving_the_team": "You are leaving the team '{{team}}'",
+ "Leave_Team": "Leave Team",
+ "Select_Team_Channels": "Select the Team's channels you would like to leave.",
+ "Cannot_leave": "Cannot leave",
+ "Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.",
+ "last-owner-can-not-be-removed": "Last owner cannot be removed",
+ "leaving_team": "leaving team",
+ "member-does-not-exist": "Member does not exist"
}
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 57c6e0c71..3891d88cb 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -769,6 +769,10 @@ const RocketChat = {
// RC 3.13.0
return this.post('teams.removeRoom', { roomId, teamId });
},
+ leaveTeam({ teamName, rooms }) {
+ // RC 3.13.0
+ return this.post('teams.leave', { teamName, rooms });
+ },
updateTeamRoom({ roomId, isDefault }) {
// RC 3.13.0
return this.post('teams.updateRoom', { roomId, isDefault });
diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js
index 75758960b..18517368c 100644
--- a/app/stacks/InsideStack.js
+++ b/app/stacks/InsideStack.js
@@ -73,6 +73,7 @@ import CreateDiscussionView from '../views/CreateDiscussionView';
import QueueListView from '../ee/omnichannel/views/QueueListView';
import AddChannelTeamView from '../views/AddChannelTeamView';
import AddExistingChannelView from '../views/AddExistingChannelView';
+import SelectListView from '../views/SelectListView';
// ChatsStackNavigator
const ChatsStack = createStackNavigator();
@@ -93,6 +94,11 @@ const ChatsStackNavigator = () => {
component={RoomActionsView}
options={RoomActionsView.navigationOptions}
/>
+
{
component={RoomInfoView}
options={RoomInfoView.navigationOptions}
/>
+
leaveRoom(room.rid, room.t)
+ showConfirmationAlert({
+ message: I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
+ onPress: () => leaveRoom(room.rid, room.t)
+ });
+ }
+
+ handleLeaveTeam = async(selected) => {
+ try {
+ const { room } = this.state;
+ const { navigation, isMasterDetail } = this.props;
+ const result = await RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) });
+
+ if (result.success) {
+ if (isMasterDetail) {
+ navigation.navigate('DrawerNavigator');
+ } else {
+ navigation.navigate('RoomsListView');
}
- ]
- );
+ }
+ } catch (e) {
+ log(e);
+ showErrorAlert(
+ e.data.error
+ ? I18n.t(e.data.error)
+ : I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_team') }),
+ I18n.t('Cannot_leave')
+ );
+ }
+ }
+
+ leaveTeam = async() => {
+ const { room } = this.state;
+ const { navigation } = this.props;
+
+ try {
+ const db = database.active;
+ const subCollection = db.get('subscriptions');
+ const teamChannels = await subCollection.query(
+ Q.where('team_id', room.teamId),
+ Q.where('team_main', null)
+ );
+
+ if (teamChannels.length) {
+ navigation.navigate('SelectListView', {
+ title: 'Leave_Team',
+ data: teamChannels,
+ infoText: 'Select_Team_Channels',
+ nextAction: data => this.handleLeaveTeam(data),
+ showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave'))
+ });
+ } else {
+ showConfirmationAlert({
+ message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
+ confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
+ onPress: () => this.handleLeaveTeam()
+ });
+ }
+ } catch (e) {
+ log(e);
+ }
}
renderRoomInfo = () => {
@@ -568,9 +616,9 @@ class RoomActionsView extends React.Component {
this.onPressTouchable({
- event: this.leaveChannel
+ event: room.teamMain ? this.leaveTeam : this.leaveChannel
})}
testID='room-actions-leave-channel'
left={() => }
@@ -880,6 +928,7 @@ const mapStateToProps = state => ({
jitsiEnabled: state.settings.Jitsi_Enabled || false,
encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version,
+ isMasterDetail: state.app.isMasterDetail,
addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'],
addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'],
addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'],
diff --git a/app/views/SelectListView.js b/app/views/SelectListView.js
new file mode 100644
index 000000000..9c886da80
--- /dev/null
+++ b/app/views/SelectListView.js
@@ -0,0 +1,146 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ View, StyleSheet, FlatList, Text
+} from 'react-native';
+import { connect } from 'react-redux';
+
+import * as List from '../containers/List';
+import sharedStyles from './Styles';
+import I18n from '../i18n';
+import * as HeaderButton from '../containers/HeaderButton';
+import StatusBar from '../containers/StatusBar';
+import { themes } from '../constants/colors';
+import { withTheme } from '../theme';
+import SafeAreaView from '../containers/SafeAreaView';
+import { animateNextTransition } from '../utils/layoutAnimation';
+import Loading from '../containers/Loading';
+
+const styles = StyleSheet.create({
+ buttonText: {
+ fontSize: 16,
+ margin: 16,
+ ...sharedStyles.textRegular
+ }
+});
+
+class SelectListView extends React.Component {
+ static propTypes = {
+ navigation: PropTypes.object,
+ route: PropTypes.object,
+ theme: PropTypes.string,
+ isMasterDetail: PropTypes.bool
+ };
+
+ constructor(props) {
+ super(props);
+ const data = props.route?.params?.data;
+ this.title = props.route?.params?.title;
+ this.infoText = props.route?.params?.infoText;
+ this.nextAction = props.route?.params?.nextAction;
+ this.showAlert = props.route?.params?.showAlert;
+ this.state = {
+ data,
+ selected: [],
+ loading: false
+ };
+ this.setHeader();
+ }
+
+ setHeader = () => {
+ const { navigation, isMasterDetail } = this.props;
+ const { selected } = this.state;
+
+ const options = {
+ headerTitle: I18n.t(this.title)
+ };
+
+ if (isMasterDetail) {
+ options.headerLeft = () => ;
+ }
+
+ options.headerRight = () => (
+
+ this.nextAction(selected)} testID='select-list-view-submit' />
+
+ );
+
+ navigation.setOptions(options);
+ }
+
+ renderInfoText = () => {
+ const { theme } = this.props;
+ return (
+
+ {I18n.t(this.infoText)}
+
+ );
+ }
+
+ isChecked = (rid) => {
+ const { selected } = this.state;
+ return selected.includes(rid);
+ }
+
+ toggleItem = (rid) => {
+ const { selected } = this.state;
+
+ animateNextTransition();
+ if (!this.isChecked(rid)) {
+ this.setState({ selected: [...selected, rid] }, () => this.setHeader());
+ } else {
+ const filterSelected = selected.filter(el => el !== rid);
+ this.setState({ selected: filterSelected }, () => this.setHeader());
+ }
+ }
+
+ renderItem = ({ item }) => {
+ const { theme } = this.props;
+ const alert = item.roles.length;
+
+ const icon = item.t === 'p' ? 'channel-private' : 'channel-public';
+ const checked = this.isChecked(item.rid, item.roles) ? 'check' : null;
+
+ return (
+ <>
+
+ (alert ? this.showAlert() : this.toggleItem(item.rid))}
+ alert={alert}
+ left={() => }
+ right={() => (checked ? : null)}
+ />
+ >
+ );
+ }
+
+ render() {
+ const { loading, data } = this.state;
+ const { theme } = this.props;
+
+ return (
+
+
+ item.rid}
+ renderItem={this.renderItem}
+ ListHeaderComponent={this.renderInfoText}
+ contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
+ keyboardShouldPersistTaps='always'
+ />
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ isMasterDetail: state.app.isMasterDetail
+});
+
+export default connect(mapStateToProps)(withTheme(SelectListView));
diff --git a/storybook/stories/List.js b/storybook/stories/List.js
index 632018054..b445a1972 100644
--- a/storybook/stories/List.js
+++ b/storybook/stories/List.js
@@ -23,6 +23,20 @@ stories.add('title and subtitle', () => (
));
+stories.add('alert', () => (
+
+
+
+
+
+
+ } alert />
+
+ } alert />
+
+
+));
+
stories.add('pressable', () => (