import React from 'react'; import PropTypes from 'prop-types'; import { Alert, Clipboard, Share } from 'react-native'; import { connect } from 'react-redux'; import ActionSheet from 'react-native-action-sheet'; import moment from 'moment'; import * as Haptics from 'expo-haptics'; import RocketChat from '../lib/rocketchat'; import database from '../lib/database'; import I18n from '../i18n'; import log from '../utils/log'; import Navigation from '../lib/Navigation'; import { getMessageTranslation } from './message/utils'; import { LISTENER } from './Toast'; import EventEmitter from '../utils/events'; import { showConfirmationAlert } from '../utils/info'; class MessageActions extends React.Component { static propTypes = { actionsHide: PropTypes.func.isRequired, room: PropTypes.object.isRequired, message: PropTypes.object, user: PropTypes.object, editInit: PropTypes.func.isRequired, reactionInit: PropTypes.func.isRequired, replyInit: PropTypes.func.isRequired, isReadOnly: PropTypes.bool, Message_AllowDeleting: PropTypes.bool, Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number, Message_AllowEditing: PropTypes.bool, Message_AllowEditing_BlockEditInMinutes: PropTypes.number, Message_AllowPinning: PropTypes.bool, Message_AllowStarring: PropTypes.bool, Message_Read_Receipt_Store_Users: PropTypes.bool }; constructor(props) { super(props); this.handleActionPress = this.handleActionPress.bind(this); } async componentDidMount() { await this.setPermissions(); const { Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users, user, room, message, isReadOnly } = this.props; // Cancel this.options = [I18n.t('Cancel')]; this.CANCEL_INDEX = 0; // Reply if (!isReadOnly) { this.options.push(I18n.t('Reply')); this.REPLY_INDEX = this.options.length - 1; } // Edit if (this.allowEdit(this.props)) { this.options.push(I18n.t('Edit')); this.EDIT_INDEX = this.options.length - 1; } // Mark as unread if (message.u && message.u._id !== user.id) { this.options.push(I18n.t('Mark_unread')); this.UNREAD_INDEX = this.options.length - 1; } // Permalink this.options.push(I18n.t('Permalink')); this.PERMALINK_INDEX = this.options.length - 1; // Copy this.options.push(I18n.t('Copy')); this.COPY_INDEX = this.options.length - 1; // Share this.options.push(I18n.t('Share')); this.SHARE_INDEX = this.options.length - 1; // Quote if (!isReadOnly) { this.options.push(I18n.t('Quote')); this.QUOTE_INDEX = this.options.length - 1; } // Star if (Message_AllowStarring) { this.options.push(I18n.t(message.starred ? 'Unstar' : 'Star')); this.STAR_INDEX = this.options.length - 1; } // Pin if (Message_AllowPinning) { this.options.push(I18n.t(message.pinned ? 'Unpin' : 'Pin')); this.PIN_INDEX = this.options.length - 1; } // Reaction if (!isReadOnly || this.canReactWhenReadOnly()) { this.options.push(I18n.t('Add_Reaction')); this.REACTION_INDEX = this.options.length - 1; } // Read Receipts if (Message_Read_Receipt_Store_Users) { this.options.push(I18n.t('Read_Receipt')); this.READ_RECEIPT_INDEX = this.options.length - 1; } // Toggle Auto-translate if (room.autoTranslate && message.u && message.u._id !== user.id) { this.options.push(I18n.t(message.autoTranslate ? 'View_Original' : 'Translate')); this.TOGGLE_TRANSLATION_INDEX = this.options.length - 1; } // Report this.options.push(I18n.t('Report')); this.REPORT_INDEX = this.options.length - 1; // Delete if (this.allowDelete(this.props)) { this.options.push(I18n.t('Delete')); this.DELETE_INDEX = this.options.length - 1; } setTimeout(() => { this.showActionSheet(); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }); } async setPermissions() { try { const { room } = this.props; const permissions = ['edit-message', 'delete-message', 'force-delete-message']; const result = await RocketChat.hasPermission(permissions, room.rid); this.hasEditPermission = result[permissions[0]]; this.hasDeletePermission = result[permissions[1]]; this.hasForceDeletePermission = result[permissions[2]]; } catch (e) { log(e); } Promise.resolve(); } showActionSheet = () => { ActionSheet.showActionSheetWithOptions({ options: this.options, cancelButtonIndex: this.CANCEL_INDEX, destructiveButtonIndex: this.DELETE_INDEX, title: I18n.t('Message_actions') }, (actionIndex) => { this.handleActionPress(actionIndex); }); } getPermalink = async(message) => { try { return await RocketChat.getPermalinkMessage(message); } catch (error) { return null; } } isOwn = props => props.message.u && props.message.u._id === props.user.id; canReactWhenReadOnly = () => { const { room } = this.props; return room.reactWhenReadOnly; } allowEdit = (props) => { if (props.isReadOnly) { return false; } const editOwn = this.isOwn(props); const { Message_AllowEditing: isEditAllowed, Message_AllowEditing_BlockEditInMinutes } = this.props; if (!(this.hasEditPermission || (isEditAllowed && editOwn))) { return false; } const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes; if (blockEditInMinutes) { let msgTs; if (props.message.ts != null) { msgTs = moment(props.message.ts); } let currentTsDiff; if (msgTs != null) { currentTsDiff = moment().diff(msgTs, 'minutes'); } return currentTsDiff < blockEditInMinutes; } return true; } allowDelete = (props) => { if (props.isReadOnly) { return false; } // Prevent from deleting thread start message when positioned inside the thread if (props.tmid && props.tmid === props.message.id) { return false; } const deleteOwn = this.isOwn(props); const { Message_AllowDeleting: isDeleteAllowed, Message_AllowDeleting_BlockDeleteInMinutes } = this.props; if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) { return false; } if (this.hasForceDeletePermission) { return true; } const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes; if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) { let msgTs; if (props.message.ts != null) { msgTs = moment(props.message.ts); } let currentTsDiff; if (msgTs != null) { currentTsDiff = moment().diff(msgTs, 'minutes'); } return currentTsDiff < blockDeleteInMinutes; } return true; } handleDelete = () => { showConfirmationAlert({ message: I18n.t('You_will_not_be_able_to_recover_this_message'), callToAction: I18n.t('Delete'), onPress: async() => { const { message } = this.props; try { await RocketChat.deleteMessage(message.id, message.subscription.id); } catch (e) { log(e); } } }); } handleEdit = () => { const { message, editInit } = this.props; editInit(message); } handleUnread = async() => { const { message, room } = this.props; const { id: messageId, ts } = message; const { rid } = room; try { const db = database.active; const result = await RocketChat.markAsUnread({ messageId }); if (result.success) { const subCollection = db.collections.get('subscriptions'); const subRecord = await subCollection.find(rid); await db.action(async() => { try { await subRecord.update(sub => sub.lastOpen = ts); } catch { // do nothing } }); Navigation.navigate('RoomsListView'); } } catch (e) { log(e); } } handleCopy = async() => { const { message } = this.props; await Clipboard.setString(message.msg); EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') }); } handleShare = async() => { const { message } = this.props; const permalink = await this.getPermalink(message); if (!permalink) { return; } Share.share({ message: permalink }); }; handleStar = async() => { const { message } = this.props; try { await RocketChat.toggleStarMessage(message.id, message.starred); EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') }); } catch (e) { log(e); } } handlePermalink = async() => { const { message } = this.props; const permalink = await this.getPermalink(message); Clipboard.setString(permalink); EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') }); } handlePin = async() => { const { message } = this.props; try { await RocketChat.togglePinMessage(message.id, message.pinned); } catch (e) { log(e); } } handleReply = () => { const { message, replyInit } = this.props; replyInit(message, true); } handleQuote = () => { const { message, replyInit } = this.props; replyInit(message, false); } handleReaction = () => { const { message, reactionInit } = this.props; reactionInit(message); } handleReadReceipt = () => { const { message } = this.props; Navigation.navigate('ReadReceiptsView', { messageId: message.id }); } handleReport = async() => { const { message } = this.props; try { await RocketChat.reportMessage(message.id); Alert.alert(I18n.t('Message_Reported')); } catch (e) { log(e); } } handleToggleTranslation = async() => { const { message, room } = this.props; try { const db = database.active; await db.action(async() => { await message.update((m) => { m.autoTranslate = !m.autoTranslate; m._updatedAt = new Date(); }); }); const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage); if (!translatedMessage) { const m = { _id: message.id, rid: message.subscription.id, u: message.u, msg: message.msg }; await RocketChat.translateMessage(m, room.autoTranslateLanguage); } } catch (e) { log(e); } } handleActionPress = (actionIndex) => { if (actionIndex) { switch (actionIndex) { case this.REPLY_INDEX: this.handleReply(); break; case this.EDIT_INDEX: this.handleEdit(); break; case this.UNREAD_INDEX: this.handleUnread(); break; case this.PERMALINK_INDEX: this.handlePermalink(); break; case this.COPY_INDEX: this.handleCopy(); break; case this.SHARE_INDEX: this.handleShare(); break; case this.QUOTE_INDEX: this.handleQuote(); break; case this.STAR_INDEX: this.handleStar(); break; case this.PIN_INDEX: this.handlePin(); break; case this.REACTION_INDEX: this.handleReaction(); break; case this.REPORT_INDEX: this.handleReport(); break; case this.DELETE_INDEX: this.handleDelete(); break; case this.READ_RECEIPT_INDEX: this.handleReadReceipt(); break; case this.TOGGLE_TRANSLATION_INDEX: this.handleToggleTranslation(); break; default: break; } } const { actionsHide } = this.props; actionsHide(); } render() { return ( null ); } } const mapStateToProps = state => ({ Message_AllowDeleting: state.settings.Message_AllowDeleting, Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowEditing: state.settings.Message_AllowEditing, Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes, Message_AllowPinning: state.settings.Message_AllowPinning, Message_AllowStarring: state.settings.Message_AllowStarring, Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users }); export default connect(mapStateToProps)(MessageActions);