Rocket.Chat.ReactNative/app/views/RoomInfoEditView/index.tsx

818 lines
24 KiB
TypeScript

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/FormTextInput';
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<ChatsStackParamList | ModalStackParamList, 'RoomInfoEditView'> {
serverVersion?: string;
encryptionEnabled: boolean;
setReadOnlyPermission: string[];
setReactWhenReadOnlyPermission: string[];
archiveRoomPermission: string[];
unarchiveRoomPermission: string[];
deleteCPermission: string[];
deletePPermission: string[];
deleteTeamPermission: string[];
}
class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfoEditViewState> {
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 (
<MultiSelect
options={MessageTypeValues.map(m => ({
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 (
<KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128}>
<StatusBar />
<SafeAreaView testID='room-info-edit-view' style={{ backgroundColor: themes[theme].backgroundColor }}>
<ScrollView
contentContainerStyle={sharedStyles.containerScrollView}
testID='room-info-edit-view-list'
{...scrollPersistTaps}>
<TouchableOpacity
style={styles.avatarContainer}
onPress={this.changeAvatar}
disabled={compareServerVersion(serverVersion || '', 'lowerThan', '3.6.0')}>
<Avatar
type={room.t}
text={room.name}
avatar={avatar?.url}
isStatic={avatar?.url}
rid={isEmpty(avatar) ? room.rid : undefined}
size={100}>
{serverVersion && compareServerVersion(serverVersion, 'lowerThan', '3.6.0') ? undefined : (
<TouchableOpacity
style={[styles.resetButton, { backgroundColor: themes[theme].dangerColor }]}
onPress={this.resetAvatar}>
<CustomIcon name='delete' color={themes[theme].backgroundColor} size={24} />
</TouchableOpacity>
)}
</Avatar>
</TouchableOpacity>
<FormTextInput
inputRef={e => {
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'
/>
<FormTextInput
inputRef={e => {
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'
/>
<FormTextInput
inputRef={e => {
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'
/>
<FormTextInput
inputRef={e => {
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 TextInput avoid appears the password fill when typing into Announcements TextInput */}
<View style={{ height: StyleSheet.hairlineWidth, overflow: 'hidden' }}>
<TextInput
style={{
height: StyleSheet.hairlineWidth
}}
/>
</View>
<FormTextInput
inputRef={e => {
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'
/>
<SwitchContainer
value={t}
leftLabelPrimary={I18n.t('Public')}
leftLabelSecondary={
room.teamMain ? I18n.t('Everyone_can_access_this_team') : I18n.t('Everyone_can_access_this_channel')
}
rightLabelPrimary={I18n.t('Private')}
rightLabelSecondary={
room.teamMain
? I18n.t('Just_invited_people_can_access_this_team')
: I18n.t('Just_invited_people_can_access_this_channel')
}
onValueChange={this.toggleRoomType}
theme={theme}
testID='room-info-edit-view-t'
/>
<SwitchContainer
value={ro}
leftLabelPrimary={I18n.t('Collaborative')}
leftLabelSecondary={
room.teamMain
? I18n.t('All_users_in_the_team_can_write_new_messages')
: I18n.t('All_users_in_the_channel_can_write_new_messages')
}
rightLabelPrimary={I18n.t('Read_Only')}
rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')}
onValueChange={this.toggleReadOnly}
disabled={!permissions['set-readonly'] || room.broadcast}
theme={theme}
testID='room-info-edit-view-ro'
/>
{ro && !room.broadcast ? (
<SwitchContainer
value={reactWhenReadOnly as boolean}
leftLabelPrimary={I18n.t('No_Reactions')}
leftLabelSecondary={I18n.t('Reactions_are_disabled')}
rightLabelPrimary={I18n.t('Allow_Reactions')}
rightLabelSecondary={I18n.t('Reactions_are_enabled')}
onValueChange={this.toggleReactions}
disabled={!permissions['set-react-when-readonly']}
theme={theme}
testID='room-info-edit-view-react-when-ro'
/>
) : null}
{room.broadcast
? [
<Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>,
<View style={[styles.divider, { borderColor: themes[theme].separatorColor }]} />
]
: null}
{serverVersion && !compareServerVersion(serverVersion, 'lowerThan', '3.0.0') ? (
<SwitchContainer
value={enableSysMes as boolean}
leftLabelPrimary={I18n.t('Hide_System_Messages')}
leftLabelSecondary={
enableSysMes
? I18n.t('Overwrites_the_server_configuration_and_use_room_config')
: I18n.t('Uses_server_configuration')
}
theme={theme}
testID='room-info-edit-switch-system-messages'
onValueChange={this.toggleHideSystemMessages}
labelContainerStyle={styles.hideSystemMessages}
leftLabelStyle={styles.systemMessagesLabel}>
{this.renderSystemMessages()}
</SwitchContainer>
) : null}
{encryptionEnabled ? (
<SwitchContainer
value={encrypted as boolean}
disabled={!t}
leftLabelPrimary={I18n.t('Encrypted')}
leftLabelSecondary={I18n.t('End_to_end_encrypted_room')}
theme={theme}
testID='room-info-edit-switch-encrypted'
onValueChange={this.toggleEncrypted}
labelContainerStyle={styles.hideSystemMessages}
leftLabelStyle={styles.systemMessagesLabel}
/>
) : null}
<TouchableOpacity
style={[
styles.buttonContainer,
{ backgroundColor: themes[theme].buttonBackground },
!this.formIsChanged() && styles.buttonContainerDisabled
]}
onPress={this.submit}
disabled={!this.formIsChanged()}
testID='room-info-edit-view-submit'>
<Text style={[styles.button, { color: themes[theme].buttonText }]} accessibilityRole='button'>
{I18n.t('SAVE')}
</Text>
</TouchableOpacity>
<View style={{ flexDirection: 'row' }}>
<TouchableOpacity
style={[
styles.buttonContainer_inverted,
styles.buttonInverted,
{ flex: 1, borderColor: themes[theme].auxiliaryText },
!this.formIsChanged() && styles.buttonContainerDisabled
]}
onPress={this.reset}
disabled={!this.formIsChanged()}
testID='room-info-edit-view-reset'>
<Text
style={[styles.button, styles.button_inverted, { color: themes[theme].bodyText }]}
accessibilityRole='button'>
{I18n.t('RESET')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.buttonInverted,
styles.buttonContainer_inverted,
archived
? !permissions['unarchive-room'] && sharedStyles.opacity5
: !permissions['archive-room'] && sharedStyles.opacity5,
{ flex: 1, marginLeft: 10, borderColor: dangerColor }
]}
onPress={this.toggleArchive}
disabled={archived ? !permissions['unarchive-room'] : !permissions['archive-room']}
testID={archived ? 'room-info-edit-view-unarchive' : 'room-info-edit-view-archive'}>
<Text style={[styles.button, styles.button_inverted, { color: dangerColor }]}>
{archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE')}
</Text>
</TouchableOpacity>
</View>
<View style={[styles.divider, { borderColor: themes[theme].separatorColor }]} />
<TouchableOpacity
style={[
styles.buttonContainer_inverted,
styles.buttonContainerLastChild,
styles.buttonDanger,
{ borderColor: dangerColor },
!this.hasDeletePermission() && sharedStyles.opacity5
]}
onPress={room.teamMain ? this.deleteTeam : this.delete}
disabled={!this.hasDeletePermission()}
testID='room-info-edit-view-delete'>
<Text style={[styles.button, styles.button_inverted, { color: dangerColor }]} accessibilityRole='button'>
{I18n.t('DELETE')}
</Text>
</TouchableOpacity>
<Loading visible={saving} />
</ScrollView>
</SafeAreaView>
</KeyboardView>
);
}
}
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));