import React from 'react'; import PropTypes from 'prop-types'; import { Alert, Keyboard, ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { connect } from 'react-redux'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import ImagePicker from 'react-native-image-crop-picker'; import { dequal } from 'dequal'; import isEmpty from 'lodash/isEmpty'; import { Q } from '@nozbe/watermelondb'; import { compareServerVersion, methods } from '../../lib/utils'; import database from '../../lib/database'; import { deleteRoom as deleteRoomAction } from '../../actions/room'; import KeyboardView from '../../presentation/KeyboardView'; import sharedStyles from '../Styles'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import { LISTENER } from '../../containers/Toast'; import EventEmitter from '../../utils/events'; import RocketChat from '../../lib/rocketchat'; import RCTextInput from '../../containers/TextInput'; import Loading from '../../containers/Loading'; import random from '../../utils/random'; import log, { events, logEvent } from '../../utils/log'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; import { themes } from '../../constants/colors'; import { withTheme } from '../../theme'; import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { MessageTypeValues } from '../../utils/messageTypes'; import SafeAreaView from '../../containers/SafeAreaView'; import Avatar from '../../containers/Avatar'; import { CustomIcon } from '../../lib/Icons'; import SwitchContainer from './SwitchContainer'; import styles from './styles'; const PERMISSION_SET_READONLY = 'set-readonly'; const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly'; const PERMISSION_ARCHIVE = 'archive-room'; const PERMISSION_UNARCHIVE = 'unarchive-room'; const PERMISSION_DELETE_C = 'delete-c'; const PERMISSION_DELETE_P = 'delete-p'; const PERMISSION_DELETE_TEAM = 'delete-team'; class RoomInfoEditView extends React.Component { static navigationOptions = () => ({ title: I18n.t('Room_Info_Edit') }); static propTypes = { navigation: PropTypes.object, route: PropTypes.object, deleteRoom: PropTypes.func, serverVersion: PropTypes.string, encryptionEnabled: PropTypes.bool, theme: PropTypes.string, setReadOnlyPermission: PropTypes.array, setReactWhenReadOnlyPermission: PropTypes.array, archiveRoomPermission: PropTypes.array, unarchiveRoomPermission: PropTypes.array, deleteCPermission: PropTypes.array, deletePPermission: PropTypes.array, deleteTeamPermission: PropTypes.array, isMasterDetail: PropTypes.bool }; constructor(props) { super(props); this.state = { room: {}, avatar: {}, permissions: {}, name: '', description: '', topic: '', announcement: '', joinCode: '', nameError: {}, saving: false, t: false, ro: false, reactWhenReadOnly: false, archived: false, systemMessages: [], enableSysMes: false, encrypted: false }; this.loadRoom(); } componentWillUnmount() { if (this.querySubscription && this.querySubscription.unsubscribe) { this.querySubscription.unsubscribe(); } } // eslint-disable-next-line react/sort-comp loadRoom = async () => { const { route, setReadOnlyPermission, setReactWhenReadOnlyPermission, archiveRoomPermission, unarchiveRoomPermission, deleteCPermission, deletePPermission, deleteTeamPermission } = this.props; const rid = route.params?.rid; if (!rid) { return; } try { const db = database.active; const sub = await db.get('subscriptions').find(rid); const observable = sub.observe(); this.querySubscription = observable.subscribe(data => { this.room = data; this.init(this.room); }); const result = await RocketChat.hasPermission( [ setReadOnlyPermission, setReactWhenReadOnlyPermission, archiveRoomPermission, unarchiveRoomPermission, deleteCPermission, deletePPermission, ...(this.room.teamMain ? [deleteTeamPermission] : []) ], rid ); this.setState({ permissions: { [PERMISSION_SET_READONLY]: result[0], [PERMISSION_SET_REACT_WHEN_READONLY]: result[1], [PERMISSION_ARCHIVE]: result[2], [PERMISSION_UNARCHIVE]: result[3], [PERMISSION_DELETE_C]: result[4], [PERMISSION_DELETE_P]: result[5], ...(this.room.teamMain && { [PERMISSION_DELETE_TEAM]: result[6] }) } }); } catch (e) { log(e); } }; init = room => { const { description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired, sysMes, encrypted } = room; // fake password just to user knows about it this.randomValue = random(15); this.setState({ room, name: RocketChat.getRoomTitle(room), description, topic, announcement, t: t === 'p', avatar: {}, ro, reactWhenReadOnly, joinCode: joinCodeRequired ? this.randomValue : '', archived: room.archived, systemMessages: sysMes, enableSysMes: sysMes && sysMes.length > 0, encrypted }); }; clearErrors = () => { this.setState({ nameError: {} }); }; reset = () => { logEvent(events.RI_EDIT_RESET); this.clearErrors(); this.init(this.room); }; formIsChanged = () => { const { room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, enableSysMes, encrypted, avatar } = this.state; const { joinCodeRequired } = room; return !( room.name === name && room.description === description && room.topic === topic && room.announcement === announcement && (joinCodeRequired ? this.randomValue : '') === joinCode && (room.t === 'p') === t && room.ro === ro && room.reactWhenReadOnly === reactWhenReadOnly && dequal(room.sysMes, systemMessages) && enableSysMes === (room.sysMes && room.sysMes.length > 0) && room.encrypted === encrypted && isEmpty(avatar) ); }; submit = async () => { logEvent(events.RI_EDIT_SAVE); Keyboard.dismiss(); const { room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted, avatar } = this.state; this.setState({ saving: true }); let error = false; if (!this.formIsChanged()) { showErrorAlert(I18n.t('Nothing_to_save')); return; } // Clear error objects await this.clearErrors(); const params = {}; // Name if (room.name !== name) { params.roomName = name; } // Avatar if (!isEmpty(avatar)) { params.roomAvatar = avatar.data; } // Description if (room.description !== description) { params.roomDescription = description; } // Topic if (room.topic !== topic) { params.roomTopic = topic; } // Announcement if (room.announcement !== announcement) { params.roomAnnouncement = announcement; } // Room Type if (room.t !== t) { params.roomType = t ? 'p' : 'c'; } // Read Only if (room.ro !== ro) { params.readOnly = ro; } // React When Read Only if (room.reactWhenReadOnly !== reactWhenReadOnly) { params.reactWhenReadOnly = reactWhenReadOnly; } if (!dequal(room.sysMes, systemMessages)) { params.systemMessages = systemMessages; } // Join Code if (this.randomValue !== joinCode) { params.joinCode = joinCode; } // Encrypted if (room.encrypted !== encrypted) { params.encrypted = encrypted; } try { await RocketChat.saveRoomSettings(room.rid, params); } catch (e) { if (e.error === 'error-invalid-room-name') { this.setState({ nameError: e }); } error = true; log(e); } await this.setState({ saving: false }); setTimeout(() => { if (error) { logEvent(events.RI_EDIT_SAVE_F); showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_settings') })); } else { EventEmitter.emit(LISTENER, { message: I18n.t('Settings_succesfully_changed') }); } }, 100); }; deleteTeam = async () => { const { room } = this.state; const { navigation, deleteCPermission, deletePPermission, deleteRoom } = 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', Q.notEq(true))); const teamChannelOwner = []; for (let i = 0; i < teamChannels.length; i += 1) { const permissionType = teamChannels[i].t === 'c' ? deleteCPermission : deletePPermission; // eslint-disable-next-line no-await-in-loop const permissions = await RocketChat.hasPermission([permissionType], teamChannels[i].rid); if (permissions[0]) { teamChannelOwner.push(teamChannels[i]); } } if (teamChannelOwner.length) { navigation.navigate('SelectListView', { title: 'Delete_Team', data: teamChannelOwner, infoText: 'Select_channels_to_delete', nextAction: selected => { showConfirmationAlert({ message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }), onPress: () => deleteRoom('team', room, selected) }); } }); } else { showConfirmationAlert({ message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }), onPress: () => deleteRoom('team', room) }); } } catch (e) { log(e); showErrorAlert( e.data.error ? I18n.t(e.data.error) : I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_team') }), I18n.t('Cannot_delete') ); } }; delete = () => { const { room } = this.state; const { deleteRoom } = this.props; Alert.alert( I18n.t('Are_you_sure_question_mark'), I18n.t('Delete_Room_Warning'), [ { text: I18n.t('Cancel'), style: 'cancel' }, { text: I18n.t('Yes_action_it', { action: I18n.t('delete') }), style: 'destructive', onPress: () => deleteRoom('channel', room) } ], { cancelable: false } ); }; toggleArchive = () => { const { room } = this.state; const { rid, archived, t } = room; const action = I18n.t(`${archived ? 'un' : ''}archive`); Alert.alert( I18n.t('Are_you_sure_question_mark'), I18n.t('Do_you_really_want_to_key_this_room_question_mark', { key: action }), [ { text: I18n.t('Cancel'), style: 'cancel' }, { text: I18n.t('Yes_action_it', { action }), style: 'destructive', onPress: async () => { try { logEvent(events.RI_EDIT_TOGGLE_ARCHIVE); await RocketChat.toggleArchiveRoom(rid, t, !archived); } catch (e) { logEvent(events.RI_EDIT_TOGGLE_ARCHIVE_F); log(e); } } } ], { cancelable: false } ); }; hasDeletePermission = () => { const { room, permissions } = this.state; if (room.teamMain) { return permissions[PERMISSION_DELETE_TEAM]; } if (room.t === 'p') { return permissions[PERMISSION_DELETE_P]; } return permissions[PERMISSION_DELETE_C]; }; renderSystemMessages = () => { const { systemMessages, enableSysMes } = this.state; const { theme } = this.props; if (!enableSysMes) { return null; } return ( ({ value: m.value, text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) } }))} onChange={({ value }) => this.setState({ systemMessages: value })} placeholder={{ text: I18n.t('Hide_System_Messages') }} value={systemMessages} context={BLOCK_CONTEXT.FORM} multiselect theme={theme} /> ); }; changeAvatar = async () => { const options = { cropping: true, compressImageQuality: 0.8, cropperAvoidEmptySpaceAroundImage: false, cropperChooseText: I18n.t('Choose'), cropperCancelText: I18n.t('Cancel'), includeBase64: true }; try { const response = await ImagePicker.openPicker(options); this.setState({ avatar: { url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' } }); } catch (e) { console.log(e); } }; resetAvatar = () => { this.setState({ avatar: { data: null } }); }; toggleRoomType = value => { logEvent(events.RI_EDIT_TOGGLE_ROOM_TYPE); this.setState(({ encrypted }) => ({ t: value, encrypted: value && encrypted })); }; toggleReadOnly = value => { logEvent(events.RI_EDIT_TOGGLE_READ_ONLY); this.setState({ ro: value }); }; toggleReactions = value => { logEvent(events.RI_EDIT_TOGGLE_REACTIONS); this.setState({ reactWhenReadOnly: value }); }; toggleHideSystemMessages = value => { logEvent(events.RI_EDIT_TOGGLE_SYSTEM_MSG); this.setState(({ systemMessages }) => ({ enableSysMes: value, systemMessages: value ? systemMessages : [] })); }; toggleEncrypted = value => { logEvent(events.RI_EDIT_TOGGLE_ENCRYPTED); this.setState({ encrypted: value }); }; render() { const { name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived, enableSysMes, encrypted, avatar } = this.state; const { serverVersion, encryptionEnabled, theme } = this.props; const { dangerColor } = themes[theme]; return ( {compareServerVersion(serverVersion, '3.6.0', methods.lowerThan) ? null : ( )} { this.name = e; }} label={I18n.t('Name')} value={name} onChangeText={value => this.setState({ name: value })} onSubmitEditing={() => { this.description.focus(); }} error={nameError} theme={theme} testID='room-info-edit-view-name' /> { this.description = e; }} label={I18n.t('Description')} value={description} onChangeText={value => this.setState({ description: value })} onSubmitEditing={() => { this.topic.focus(); }} theme={theme} testID='room-info-edit-view-description' /> { this.topic = e; }} label={I18n.t('Topic')} value={topic} onChangeText={value => this.setState({ topic: value })} onSubmitEditing={() => { this.announcement.focus(); }} theme={theme} testID='room-info-edit-view-topic' /> { this.announcement = e; }} label={I18n.t('Announcement')} value={announcement} onChangeText={value => this.setState({ announcement: value })} onSubmitEditing={() => { this.joinCode.focus(); }} theme={theme} testID='room-info-edit-view-announcement' /> { this.joinCode = e; }} label={I18n.t('Password')} value={joinCode} onChangeText={value => this.setState({ joinCode: value })} onSubmitEditing={this.submit} secureTextEntry theme={theme} testID='room-info-edit-view-password' /> {ro && !room.broadcast ? ( ) : null} {room.broadcast ? [ {I18n.t('Broadcast_Channel')}, ] : null} {!compareServerVersion(serverVersion, '3.0.0', methods.lowerThan) ? ( {this.renderSystemMessages()} ) : null} {encryptionEnabled ? ( ) : null} {I18n.t('SAVE')} {I18n.t('RESET')} {archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE')} {I18n.t('DELETE')} ); } } const mapStateToProps = state => ({ serverVersion: state.server.version, encryptionEnabled: state.encryption.enabled, setReadOnlyPermission: state.permissions[PERMISSION_SET_READONLY], setReactWhenReadOnlyPermission: state.permissions[PERMISSION_SET_REACT_WHEN_READONLY], archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE], unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE], deleteCPermission: state.permissions[PERMISSION_DELETE_C], deletePPermission: state.permissions[PERMISSION_DELETE_P], deleteTeamPermission: state.permissions[PERMISSION_DELETE_TEAM], isMasterDetail: state.app.isMasterDetail }); const mapDispatchToProps = dispatch => ({ deleteRoom: (roomType, room, selected) => dispatch(deleteRoomAction(roomType, room, selected)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RoomInfoEditView));