import React from 'react'; import PropTypes from 'prop-types'; import { View, SectionList, Text, Alert, Share, Switch } from 'react-native'; import { connect } from 'react-redux'; import _ from 'lodash'; import Touch from '../../utils/touch'; import { setLoading as setLoadingAction } from '../../actions/selectedUsers'; import { leaveRoom as leaveRoomAction, closeRoom as closeRoomAction } from '../../actions/room'; import styles from './styles'; import sharedStyles from '../Styles'; import Avatar from '../../containers/Avatar'; import Status from '../../containers/Status'; import RocketChat from '../../lib/rocketchat'; import log, { logEvent, events } from '../../utils/log'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; import I18n from '../../i18n'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; import { CustomIcon } from '../../lib/Icons'; import DisclosureIndicator from '../../containers/DisclosureIndicator'; import StatusBar from '../../containers/StatusBar'; import { themes, SWITCH_TRACK_COLOR } from '../../constants/colors'; import { withTheme } from '../../theme'; import { CloseModalButton } from '../../containers/HeaderButton'; import { getUserSelector } from '../../selectors/login'; import Markdown from '../../containers/markdown'; import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import SafeAreaView from '../../containers/SafeAreaView'; import { E2E_ROOM_TYPES } from '../../lib/encryption/constants'; import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import database from '../../lib/database'; class RoomActionsView extends React.Component { static navigationOptions = ({ navigation, isMasterDetail }) => { const options = { title: I18n.t('Actions') }; if (isMasterDetail) { options.headerLeft = () => ; } return options; } static propTypes = { baseUrl: PropTypes.string, navigation: PropTypes.object, route: PropTypes.object, user: PropTypes.shape({ id: PropTypes.string, token: PropTypes.string }), leaveRoom: PropTypes.func, jitsiEnabled: PropTypes.bool, e2eEnabled: PropTypes.bool, setLoadingInvite: PropTypes.func, closeRoom: PropTypes.func, theme: PropTypes.string } constructor(props) { super(props); this.mounted = false; const room = props.route.params?.room; const member = props.route.params?.member; this.rid = props.route.params?.rid; this.t = props.route.params?.t; this.state = { room: room || { rid: this.rid, t: this.t }, membersCount: 0, member: member || {}, joined: !!room, canViewMembers: false, canAutoTranslate: false, canAddUser: false, canInviteUser: false, canForwardGuest: false, canReturnQueue: false, canEdit: false }; if (room && room.observe && room.rid) { this.roomObservable = room.observe(); this.subscription = this.roomObservable .subscribe((changes) => { if (this.mounted) { this.setState({ room: changes }); } else { this.state.room = changes; } }); } } async componentDidMount() { this.mounted = true; const { room, member } = this.state; if (room.rid) { if (!room.id && !this.isOmnichannelPreview) { try { const result = await RocketChat.getChannelInfo(room.rid); if (result.success) { this.setState({ room: { ...result.channel, rid: result.channel._id } }); } } catch (e) { log(e); } } if (room && room.t !== 'd' && this.canViewMembers()) { try { const counters = await RocketChat.getRoomCounters(room.rid, room.t); if (counters.success) { this.setState({ membersCount: counters.members, joined: counters.joined }); } } catch (e) { log(e); } } else if (room.t === 'd' && _.isEmpty(member)) { this.updateRoomMember(); } const canAutoTranslate = await RocketChat.canAutoTranslate(); this.setState({ canAutoTranslate }); this.canAddUser(); this.canInviteUser(); this.canEdit(); // livechat permissions if (room.t === 'l') { this.canForwardGuest(); this.canReturnQueue(); } } } componentWillUnmount() { if (this.subscription && this.subscription.unsubscribe) { this.subscription.unsubscribe(); } } get isOmnichannelPreview() { const { room } = this.state; return room.t === 'l' && room.status === 'queued'; } onPressTouchable = (item) => { const { route, event, params } = item; if (route) { logEvent(events[`RA_GO_${ route.replace('View', '').toUpperCase() }${ params.name ? params.name.toUpperCase() : '' }`]); const { navigation } = this.props; navigation.navigate(route, params); } if (event) { return event(); } } // eslint-disable-next-line react/sort-comp canAddUser = async() => { const { room, joined } = this.state; const { rid, t } = room; let canAdd = false; const userInRoom = joined; const permissions = await RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid); if (permissions) { if (userInRoom && permissions['add-user-to-joined-room']) { canAdd = true; } if (t === 'c' && permissions['add-user-to-any-c-room']) { canAdd = true; } if (t === 'p' && permissions['add-user-to-any-p-room']) { canAdd = true; } } this.setState({ canAddUser: canAdd }); } canInviteUser = async() => { const { room } = this.state; const { rid } = room; const permissions = await RocketChat.hasPermission(['create-invite-links'], rid); const canInviteUser = permissions && permissions['create-invite-links']; this.setState({ canInviteUser }); } canEdit = async() => { const { room } = this.state; const { rid } = room; const permissions = await RocketChat.hasPermission(['edit-room'], rid); const canEdit = permissions && permissions['edit-room']; this.setState({ canEdit }); } canViewMembers = async() => { const { room } = this.state; const { rid, t, broadcast } = room; if (broadcast) { const viewBroadcastMemberListPermission = 'view-broadcast-member-list'; const permissions = await RocketChat.hasPermission([viewBroadcastMemberListPermission], rid); if (!permissions[viewBroadcastMemberListPermission]) { return false; } } // This method is executed only in componentDidMount and returns a value // We save the state to read in render const result = (t === 'c' || t === 'p'); this.setState({ canViewMembers: result }); return result; } canForwardGuest = async() => { const { room } = this.state; const { rid } = room; let result = true; const transferLivechatGuest = 'transfer-livechat-guest'; const permissions = await RocketChat.hasPermission([transferLivechatGuest], rid); if (!permissions[transferLivechatGuest]) { result = false; } this.setState({ canForwardGuest: result }); } canReturnQueue = async() => { try { const { returnQueue } = await RocketChat.getRoutingConfig(); this.setState({ canReturnQueue: returnQueue }); } catch { // do nothing } } get sections() { const { room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue, canEdit } = this.state; const { jitsiEnabled, e2eEnabled } = this.props; const { rid, t, blocker, encrypted } = room; const isGroupChat = RocketChat.isGroupChat(room); const notificationsAction = { icon: 'notification', name: I18n.t('Notifications'), route: 'NotificationPrefView', params: { rid, room }, testID: 'room-actions-notifications', right: this.renderDisclosure }; const jitsiActions = jitsiEnabled ? [ { icon: 'phone', name: I18n.t('Voice_call'), event: () => RocketChat.callJitsi(rid, true), testID: 'room-actions-voice', right: this.renderDisclosure }, { icon: 'camera', name: I18n.t('Video_call'), event: () => RocketChat.callJitsi(rid), testID: 'room-actions-video', right: this.renderDisclosure } ] : []; const sections = [{ data: [{ icon: 'star', name: I18n.t('Room_Info'), route: 'RoomInfoView', // forward room only if room isn't joined params: { rid, t, room, member }, disabled: isGroupChat, testID: 'room-actions-info' }], renderItem: this.renderRoomInfo }, { data: jitsiActions, renderItem: this.renderItem }, { data: [ { icon: 'attach', name: I18n.t('Files'), route: 'MessagesView', params: { rid, t, name: 'Files' }, testID: 'room-actions-files', right: this.renderDisclosure }, { icon: 'mention', name: I18n.t('Mentions'), route: 'MessagesView', params: { rid, t, name: 'Mentions' }, testID: 'room-actions-mentioned', right: this.renderDisclosure }, { icon: 'star', name: I18n.t('Starred'), route: 'MessagesView', params: { rid, t, name: 'Starred' }, testID: 'room-actions-starred', right: this.renderDisclosure }, { icon: 'search', name: I18n.t('Search'), route: 'SearchMessagesView', params: { rid, encrypted }, testID: 'room-actions-search', right: this.renderDisclosure }, { icon: 'share', name: I18n.t('Share'), event: this.handleShare, testID: 'room-actions-share', right: this.renderDisclosure }, { icon: 'pin', name: I18n.t('Pinned'), route: 'MessagesView', params: { rid, t, name: 'Pinned' }, testID: 'room-actions-pinned', right: this.renderDisclosure } ], renderItem: this.renderItem }]; if (canAutoTranslate) { sections[2].data.push({ icon: 'language', name: I18n.t('Auto_Translate'), route: 'AutoTranslateView', params: { rid, room }, testID: 'room-actions-auto-translate', right: this.renderDisclosure }); } if (isGroupChat) { sections[2].data.unshift({ icon: 'team', name: I18n.t('Members'), description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null, route: 'RoomMembersView', params: { rid, room }, testID: 'room-actions-members', right: this.renderDisclosure }); } if (t === 'd' && !isGroupChat) { sections.push({ data: [ { icon: 'ban', name: I18n.t(`${ blocker ? 'Unblock' : 'Block' }_user`), type: 'danger', event: this.toggleBlockUser, testID: 'room-actions-block-user', right: this.renderDisclosure } ], renderItem: this.renderItem }); sections[2].data.push(notificationsAction); } else if (t === 'c' || t === 'p') { const actions = []; if (canViewMembers) { actions.push({ icon: 'team', name: I18n.t('Members'), description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null, route: 'RoomMembersView', params: { rid, room }, testID: 'room-actions-members', right: this.renderDisclosure }); } if (canAddUser) { actions.push({ icon: 'add', name: I18n.t('Add_users'), route: 'SelectedUsersView', params: { rid, title: I18n.t('Add_users'), nextAction: this.addUser }, testID: 'room-actions-add-user', right: this.renderDisclosure }); } if (canInviteUser) { actions.push({ icon: 'user-add', name: I18n.t('Invite_users'), route: 'InviteUsersView', params: { rid }, testID: 'room-actions-invite-user', right: this.renderDisclosure }); } sections[2].data = [...actions, ...sections[2].data]; if (joined) { sections[2].data.push(notificationsAction); sections.push({ data: [ { icon: 'logout', name: I18n.t('Leave_channel'), type: 'danger', event: this.leaveChannel, testID: 'room-actions-leave-channel', right: this.renderDisclosure } ], renderItem: this.renderItem }); } } else if (t === 'l') { sections[2].data = []; if (!this.isOmnichannelPreview) { sections[2].data.push({ icon: 'close', name: I18n.t('Close'), event: this.closeLivechat, right: this.renderDisclosure }); if (canForwardGuest) { sections[2].data.push({ icon: 'user-forward', name: I18n.t('Forward'), route: 'ForwardLivechatView', params: { rid }, right: this.renderDisclosure }); } if (canReturnQueue) { sections[2].data.push({ icon: 'undo', name: I18n.t('Return'), event: this.returnLivechat, right: this.renderDisclosure }); } sections[2].data.push({ icon: 'history', name: I18n.t('Navigation_history'), route: 'VisitorNavigationView', params: { rid }, right: this.renderDisclosure }); } sections.push({ data: [notificationsAction], renderItem: this.renderItem }); } // If can edit this room // If this room type can be Encrypted // If e2e is enabled for this server if (canEdit && E2E_ROOM_TYPES[t] && e2eEnabled) { sections.splice(2, 0, { data: [{ icon: 'encrypted', name: I18n.t('Encrypted'), testID: 'room-actions-encrypt', right: this.renderEncryptedSwitch }], renderItem: this.renderItem }); } return sections; } renderDisclosure = () => { const { theme } = this.props; return ; } renderSeparator = () => { const { theme } = this.props; return ; } renderEncryptedSwitch = () => { const { room } = this.state; const { encrypted } = room; return ( ); } closeLivechat = () => { const { room: { rid } } = this.state; const { closeRoom } = this.props; closeRoom(rid); } returnLivechat = () => { const { room: { rid } } = this.state; showConfirmationAlert({ message: I18n.t('Would_you_like_to_return_the_inquiry'), confirmationText: I18n.t('Yes'), onPress: async() => { try { await RocketChat.returnLivechat(rid); } catch (e) { showErrorAlert(e.reason, I18n.t('Oops')); } } }); } updateRoomMember = async() => { const { room } = this.state; try { if (!RocketChat.isGroupChat(room)) { const roomUserId = RocketChat.getUidDirectMessage(room); const result = await RocketChat.getUserInfo(roomUserId); if (result.success) { this.setState({ member: result.user }); } } } catch (e) { log(e); this.setState({ member: {} }); } } addUser = async() => { const { room } = this.state; const { setLoadingInvite, navigation } = this.props; const { rid } = room; try { setLoadingInvite(true); await RocketChat.addUsersToRoom(rid); navigation.pop(); } catch (e) { log(e); } finally { setLoadingInvite(false); } } toggleBlockUser = async() => { logEvent(events.RA_TOGGLE_BLOCK_USER); const { room } = this.state; const { rid, blocker } = room; const { member } = this.state; try { await RocketChat.toggleBlockUser(rid, member._id, !blocker); } catch (e) { logEvent(events.RA_TOGGLE_BLOCK_USER_F); log(e); } } toggleEncrypted = async() => { logEvent(events.RA_TOGGLE_ENCRYPTED); const { room } = this.state; const { rid } = room; const db = database.active; // Toggle encrypted value const encrypted = !room.encrypted; try { // Instantly feedback to the user await db.action(async() => { await room.update(protectedFunction((r) => { r.encrypted = encrypted; })); }); try { // Send new room setting value to server const { result } = await RocketChat.saveRoomSettings(rid, { encrypted }); // If it was saved successfully if (result) { return; } } catch { // do nothing } // If something goes wrong we go back to the previous value await db.action(async() => { await room.update(protectedFunction((r) => { r.encrypted = room.encrypted; })); }); } catch (e) { logEvent(events.RA_TOGGLE_ENCRYPTED_F); log(e); } } handleShare = () => { logEvent(events.RA_SHARE); const { room } = this.state; const permalink = RocketChat.getPermalinkChannel(room); if (!permalink) { return; } Share.share({ message: permalink }); }; leaveChannel = () => { const { room } = this.state; const { leaveRoom } = this.props; Alert.alert( I18n.t('Are_you_sure_question_mark'), I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }), [ { text: I18n.t('Cancel'), style: 'cancel' }, { text: I18n.t('Yes_action_it', { action: I18n.t('leave') }), style: 'destructive', onPress: () => leaveRoom(room.rid, room.t) } ] ); } renderRoomInfo = ({ item }) => { const { room, member } = this.state; const { name, t, rid, topic } = room; const { theme } = this.props; const avatar = RocketChat.getRoomAvatar(room); return ( this.renderTouchableItem(( <> {t === 'd' && member._id ? : null } {room.t === 'd' ? {room.fname} : ( {RocketChat.getRoomTitle(room)} ) } {room.t === 'd' && } {!item.disabled && } ), item) ); } renderTouchableItem = (subview, item) => { const { theme } = this.props; return ( this.onPressTouchable(item)} style={{ backgroundColor: themes[theme].backgroundColor }} accessibilityLabel={item.name} accessibilityTraits='button' enabled={!item.disabled} testID={item.testID} theme={theme} > {subview} ); } renderItem = ({ item }) => { const { theme } = this.props; const colorDanger = { color: themes[theme].dangerColor }; const subview = item.type === 'danger' ? ( <> { item.name } ) : ( <> { item.name } {item.description ? { item.description } : null} {item?.right?.()} ); return this.renderTouchableItem(subview, item); } renderSectionSeparator = (data) => { const { theme } = this.props; if (data.trailingItem) { return ; } if (!data.trailingSection) { return ; } return null; } render() { const { theme } = this.props; return ( item.name} testID='room-actions-list' {...scrollPersistTaps} /> ); } } const mapStateToProps = state => ({ user: getUserSelector(state), baseUrl: state.server.server, jitsiEnabled: state.settings.Jitsi_Enabled || false, e2eEnabled: state.settings.E2E_Enable || false }); const mapDispatchToProps = dispatch => ({ leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)), closeRoom: rid => dispatch(closeRoomAction(rid)), setLoadingInvite: loading => dispatch(setLoadingAction(loading)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RoomActionsView));