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', () => (