import React from 'react'; import { Q } from '@nozbe/watermelondb'; import { BlockContext } from '@rocket.chat/ui-kit'; import { dequal } from 'dequal'; import isEmpty from 'lodash/isEmpty'; import { Alert, Keyboard, ScrollView, Text, TextInput, TouchableOpacity, View, StyleSheet } from 'react-native'; import ImagePicker, { Image } from 'react-native-image-crop-picker'; import { connect } from 'react-redux'; import { deleteRoom } from '../../actions/room'; import { themes } from '../../lib/constants'; import Avatar from '../../containers/Avatar'; import Loading from '../../containers/Loading'; import SafeAreaView from '../../containers/SafeAreaView'; import StatusBar from '../../containers/StatusBar'; import { FormTextInput } from '../../containers/TextInput'; import { LISTENER } from '../../containers/Toast'; import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { IApplicationState, IBaseScreen, IRoomSettings, ISubscription, SubscriptionType, TSubscriptionModel, IAvatar } from '../../definitions'; import { ERoomType } from '../../definitions/ERoomType'; import I18n from '../../i18n'; import database from '../../lib/database'; import { CustomIcon } from '../../containers/CustomIcon'; import KeyboardView from '../../containers/KeyboardView'; import { TSupportedPermissions } from '../../reducers/permissions'; import { ModalStackParamList } from '../../stacks/MasterDetailStack/types'; import { ChatsStackParamList } from '../../stacks/types'; import { withTheme } from '../../theme'; import EventEmitter from '../../lib/methods/helpers/events'; import log, { events, logEvent } from '../../lib/methods/helpers/log'; import { MessageTypeValues } from './messageTypes'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import sharedStyles from '../Styles'; import styles from './styles'; import SwitchContainer from './SwitchContainer'; import { getRoomTitle, hasPermission, compareServerVersion, showConfirmationAlert, showErrorAlert, random } from '../../lib/methods/helpers'; import { Services } from '../../lib/services'; interface IRoomInfoEditViewState { room: ISubscription; avatar: IAvatar; permissions: { [key in TSupportedPermissions]?: boolean }; name: string; description?: string; topic?: string; announcement?: string; joinCode: string; nameError: any; saving: boolean; t: boolean; ro: boolean; reactWhenReadOnly?: boolean; archived: boolean; systemMessages?: boolean | string[]; enableSysMes?: boolean | string[]; encrypted?: boolean; } interface IRoomInfoEditViewProps extends IBaseScreen { serverVersion?: string; encryptionEnabled: boolean; setReadOnlyPermission: string[]; setReactWhenReadOnlyPermission: string[]; archiveRoomPermission: string[]; unarchiveRoomPermission: string[]; deleteCPermission: string[]; deletePPermission: string[]; deleteTeamPermission: string[]; } class RoomInfoEditView extends React.Component { randomValue = random(15); private room: TSubscriptionModel; private name: TextInput | null | undefined; private description: TextInput | null | undefined; private topic: TextInput | null | undefined; private announcement: TextInput | null | undefined; private joinCode: TextInput | null | undefined; static navigationOptions = () => ({ title: I18n.t('Room_Info_Edit') }); constructor(props: IRoomInfoEditViewProps) { super(props); this.room = {} as TSubscriptionModel; this.state = { room: {} as ISubscription, avatar: {} as IAvatar, permissions: {}, name: '', description: '', topic: '', announcement: '', joinCode: '', nameError: {}, saving: false, t: false, ro: false, reactWhenReadOnly: false, archived: false, systemMessages: [], enableSysMes: false, encrypted: false }; this.loadRoom(); } 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); this.room = sub; this.init(this.room); const result = await hasPermission( [ setReadOnlyPermission, setReactWhenReadOnlyPermission, archiveRoomPermission, unarchiveRoomPermission, deleteCPermission, deletePPermission, ...(this.room.teamMain ? [deleteTeamPermission] : []) ], rid ); this.setState({ permissions: { 'set-readonly': result[0], 'set-react-when-readonly': result[1], 'archive-room': result[2], 'unarchive-room': result[3], 'delete-c': result[4], 'delete-p': result[5], ...(this.room.teamMain && { 'delete-team': result[6] }) } }); } catch (e) { log(e); } }; init = (room: ISubscription) => { const { description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired, encrypted } = room; const sysMes = room.sysMes as string[]; // fake password just to user knows about it this.setState({ room, name: getRoomTitle(room), description, topic, announcement, t: t === 'p', avatar: {} as IAvatar, 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; const sysMes = room.sysMes as string[]; 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(sysMes, systemMessages) && enableSysMes === (sysMes && 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 = {} as IRoomSettings; // Name if (room.name !== name) { params.roomName = name; } // Avatar if (!isEmpty(avatar)) { params.roomAvatar = avatar.data as string; } // 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 === SubscriptionType.GROUP) !== t) { params.roomType = t ? ('p' as SubscriptionType) : ('c' as SubscriptionType); } // 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 as string[]; } // Join Code if (this.randomValue !== joinCode) { params.joinCode = joinCode; } // Encrypted if (room.encrypted !== encrypted) { params.encrypted = encrypted; } try { await Services.saveRoomSettings(room.rid, params); } catch (e: any) { 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, dispatch } = this.props; try { const db = database.active; const subCollection = db.get('subscriptions'); const teamChannels = await subCollection .query(Q.where('team_id', room.teamId as string), Q.where('team_main', Q.notEq(true))) .fetch(); 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 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: string[]) => { showConfirmationAlert({ message: I18n.t('You_are_deleting_the_team', { team: getRoomTitle(room) }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }), onPress: () => deleteRoom(ERoomType.t, room, selected) }); } }); } else { showConfirmationAlert({ message: I18n.t('You_are_deleting_the_team', { team: getRoomTitle(room) }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }), onPress: () => dispatch(deleteRoom(ERoomType.t, room)) }); } } catch (e: any) { 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 { dispatch } = 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: () => dispatch(deleteRoom(ERoomType.c, 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 Services.toggleArchiveRoom(rid, t as SubscriptionType, !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['delete-team']; } if (room.t === 'p') { return permissions['delete-p']; } return permissions['delete-c']; }; renderSystemMessages = () => { const { systemMessages, enableSysMes } = this.state; if (!enableSysMes) { return null; } return ( ({ value: m.value, text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) } }))} onChange={({ value }: { value: boolean }) => this.setState({ systemMessages: value })} placeholder={{ text: I18n.t('Hide_System_Messages') }} value={systemMessages as string[]} context={BlockContext.FORM} multiselect /> ); }; changeAvatar = async () => { const options = { cropping: true, compressImageQuality: 0.8, cropperAvoidEmptySpaceAroundImage: false, cropperChooseText: I18n.t('Choose'), cropperCancelText: I18n.t('Cancel'), includeBase64: true }; try { const response: Image = 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: boolean) => { logEvent(events.RI_EDIT_TOGGLE_ROOM_TYPE); this.setState(({ encrypted }) => ({ t: value, encrypted: value && encrypted })); }; toggleReadOnly = (value: boolean) => { logEvent(events.RI_EDIT_TOGGLE_READ_ONLY); this.setState({ ro: value }); }; toggleReactions = (value: boolean) => { logEvent(events.RI_EDIT_TOGGLE_REACTIONS); this.setState({ reactWhenReadOnly: value }); }; toggleHideSystemMessages = (value: boolean) => { logEvent(events.RI_EDIT_TOGGLE_SYSTEM_MSG); this.setState(({ systemMessages }) => ({ enableSysMes: value, systemMessages: value ? systemMessages : [] })); }; toggleEncrypted = (value: boolean) => { 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 ( {serverVersion && compareServerVersion(serverVersion, 'lowerThan', '3.6.0') ? undefined : ( )} { this.name = e; }} label={I18n.t('Name')} value={name} onChangeText={value => this.setState({ name: value })} onSubmitEditing={() => { this.description?.focus(); }} error={nameError} 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(); }} 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(); }} 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(); }} testID='room-info-edit-view-announcement' /> {/* This TextInput avoid appears the password fill when typing into Announcements TextInput */} { this.joinCode = e; }} label={I18n.t('Password')} value={joinCode} onChangeText={value => this.setState({ joinCode: value })} onSubmitEditing={this.submit} secureTextEntry testID='room-info-edit-view-password' /> {ro && !room.broadcast ? ( ) : null} {room.broadcast ? [ {I18n.t('Broadcast_Channel')}, ] : null} {serverVersion && !compareServerVersion(serverVersion, 'lowerThan', '3.0.0') ? ( {this.renderSystemMessages()} ) : null} {encryptionEnabled ? ( ) : null} {I18n.t('SAVE')} {I18n.t('RESET')} {archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE')} {I18n.t('DELETE')} ); } } const mapStateToProps = (state: IApplicationState) => ({ serverVersion: state.server.version as string, encryptionEnabled: state.encryption.enabled, setReadOnlyPermission: state.permissions['set-readonly'] as string[], setReactWhenReadOnlyPermission: state.permissions['set-react-when-readonly'] as string[], archiveRoomPermission: state.permissions['archive-room'] as string[], unarchiveRoomPermission: state.permissions['unarchive-room'] as string[], deleteCPermission: state.permissions['delete-c'] as string[], deletePPermission: state.permissions['delete-p'] as string[], deleteTeamPermission: state.permissions['delete-team'] as string[], isMasterDetail: state.app.isMasterDetail }); export default connect(mapStateToProps)(withTheme(RoomInfoEditView));