diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 56fb4f21b0f597b54c77e86685fc4c61706a69f3..961b88eee941250c8ee3c6cc16d16a2024a952d6 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -65,8 +65,8 @@ export const MESSAGES = createRequestTypes('MESSAGES', [ 'TOGGLE_PIN_REQUEST', 'TOGGLE_PIN_SUCCESS', 'TOGGLE_PIN_FAILURE', - 'SET_INPUT', - 'CLEAR_INPUT', + 'REPLY_INIT', + 'REPLY_CANCEL', 'TOGGLE_REACTION_PICKER', 'REPLY_BROADCAST' ]); diff --git a/app/actions/messages.js b/app/actions/messages.js index b7b3ee6909abe154c1a1344dcda934b095b2de29..1b8451c7b5bc4e50454e7fbb119e26831bb0ddc9 100644 --- a/app/actions/messages.js +++ b/app/actions/messages.js @@ -137,16 +137,17 @@ export function togglePinFailure(err) { }; } -export function setInput(message) { +export function replyInit(message, mention) { return { - type: types.MESSAGES.SET_INPUT, - message + type: types.MESSAGES.REPLY_INIT, + message, + mention }; } -export function clearInput() { +export function replyCancel() { return { - type: types.MESSAGES.CLEAR_INPUT + type: types.MESSAGES.REPLY_CANCEL }; } diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js index ccb25a79d995809b2d428d99a083bf2efd0f5141..7eec64339436ef3e66726ccc33c29bde57b9aae1 100644 --- a/app/containers/MessageActions.js +++ b/app/containers/MessageActions.js @@ -10,9 +10,9 @@ import { editInit, toggleStarRequest, togglePinRequest, - setInput, actionsHide, - toggleReactionPicker + toggleReactionPicker, + replyInit } from '../actions/messages'; import { showToast } from '../utils/info'; import RocketChat from '../lib/rocketchat'; @@ -34,8 +34,8 @@ import I18n from '../i18n'; editInit: message => dispatch(editInit(message)), toggleStarRequest: message => dispatch(toggleStarRequest(message)), togglePinRequest: message => dispatch(togglePinRequest(message)), - setInput: message => dispatch(setInput(message)), - toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) + toggleReactionPicker: message => dispatch(toggleReactionPicker(message)), + replyInit: (message, mention) => dispatch(replyInit(message, mention)) }) ) export default class MessageActions extends React.Component { @@ -43,13 +43,13 @@ export default class MessageActions extends React.Component { actionsHide: PropTypes.func.isRequired, room: PropTypes.object.isRequired, actionMessage: PropTypes.object, - user: PropTypes.object.isRequired, + // user: PropTypes.object.isRequired, deleteRequest: PropTypes.func.isRequired, editInit: PropTypes.func.isRequired, toggleStarRequest: PropTypes.func.isRequired, togglePinRequest: PropTypes.func.isRequired, - setInput: PropTypes.func.isRequired, toggleReactionPicker: PropTypes.func.isRequired, + replyInit: PropTypes.func.isRequired, Message_AllowDeleting: PropTypes.bool, Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number, Message_AllowEditing: PropTypes.bool, @@ -248,21 +248,12 @@ export default class MessageActions extends React.Component { this.props.togglePinRequest(this.props.actionMessage); } - async handleReply() { - const permalink = await this.getPermalink(this.props.actionMessage); - let msg = `[ ](${ permalink }) `; - - // if original message wasn't sent by current user and neither from a direct room - if (this.props.user.username !== this.props.actionMessage.u.username && this.props.room.t !== 'd') { - msg += `@${ this.props.actionMessage.u.username } `; - } - this.props.setInput({ msg }); + handleReply() { + this.props.replyInit(this.props.actionMessage, true); } - async handleQuote() { - const permalink = await this.getPermalink(this.props.actionMessage); - const msg = `[ ](${ permalink }) `; - this.props.setInput({ msg }); + handleQuote() { + this.props.replyInit(this.props.actionMessage, false); } handleReaction() { diff --git a/app/containers/MessageBox/ReplyPreview.js b/app/containers/MessageBox/ReplyPreview.js new file mode 100644 index 0000000000000000000000000000000000000000..d71816c4082e9ab87f5c39a945ae8ab7f16701b4 --- /dev/null +++ b/app/containers/MessageBox/ReplyPreview.js @@ -0,0 +1,78 @@ +import React, { Component } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { connect } from 'react-redux'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +import Markdown from '../message/Markdown'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'row' + }, + messageContainer: { + flex: 1, + marginHorizontal: 15, + backgroundColor: '#F3F4F5', + paddingHorizontal: 15, + paddingVertical: 10, + borderRadius: 2 + }, + header: { + flexDirection: 'row', + alignItems: 'center' + }, + username: { + color: '#1D74F5', + fontSize: 16, + fontWeight: '500' + }, + time: { + color: '#9EA2A8', + fontSize: 12, + lineHeight: 16, + marginLeft: 5 + }, + content: { + color: '#0C0D0F', + fontSize: 16, + lineHeight: 20 + }, + close: { + marginRight: 15 + } +}); + +@connect(state => ({ + Message_TimeFormat: state.settings.Message_TimeFormat +})) +export default class ReplyPreview extends Component { + static propTypes = { + message: PropTypes.object.isRequired, + Message_TimeFormat: PropTypes.string.isRequired, + close: PropTypes.func.isRequired + } + + close = () => { + this.props.close(); + } + + render() { + const { message, Message_TimeFormat } = this.props; + const time = moment(message.ts).format(Message_TimeFormat); + return ( + <View style={styles.container}> + <View style={styles.messageContainer}> + <View style={styles.header}> + <Text style={styles.username}>{message.u.username}</Text> + <Text style={styles.time}>{time}</Text> + </View> + <Markdown msg={message.msg} /> + </View> + <Icon name='close' size={20} style={styles.close} onPress={this.close} /> + </View> + ); + } +} diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index c16cc3f81f601a5e34e57b057de2eee5b859a3ab..63eb3bb12a0f725fa39d19c8448bbbd037e9b9c0 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -9,7 +9,7 @@ import ImagePicker from 'react-native-image-crop-picker'; import { userTyping } from '../../actions/room'; import RocketChat from '../../lib/rocketchat'; -import { editRequest, editCancel, clearInput } from '../../actions/messages'; +import { editRequest, editCancel, replyCancel } from '../../actions/messages'; import styles from './styles'; import MyIcon from '../icons'; import database from '../../lib/realm'; @@ -22,6 +22,7 @@ import UploadModal from './UploadModal'; import './EmojiKeyboard'; import log from '../../utils/log'; import I18n from '../../i18n'; +import ReplyPreview from './ReplyPreview'; const MENTIONS_TRACKING_TYPE_USERS = '@'; const MENTIONS_TRACKING_TYPE_EMOJIS = ':'; @@ -39,27 +40,34 @@ const imagePickerConfig = { }; @connect(state => ({ - room: state.room, + roomType: state.room.t, message: state.messages.message, + replyMessage: state.messages.replyMessage, + replying: state.messages.replyMessage && !!state.messages.replyMessage.msg, editing: state.messages.editing, - baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', + username: state.login.user && state.login.user.username }), dispatch => ({ editCancel: () => dispatch(editCancel()), editRequest: message => dispatch(editRequest(message)), typing: status => dispatch(userTyping(status)), - clearInput: () => dispatch(clearInput()) + closeReply: () => dispatch(replyCancel()) })) export default class MessageBox extends React.PureComponent { static propTypes = { - onSubmit: PropTypes.func.isRequired, rid: PropTypes.string.isRequired, - editCancel: PropTypes.func.isRequired, - editRequest: PropTypes.func.isRequired, baseUrl: PropTypes.string.isRequired, message: PropTypes.object, + replyMessage: PropTypes.object, + replying: PropTypes.bool, editing: PropTypes.bool, + username: PropTypes.string, + roomType: PropTypes.string, + editCancel: PropTypes.func.isRequired, + editRequest: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, typing: PropTypes.func, - clearInput: PropTypes.func + closeReply: PropTypes.func } constructor(props) { @@ -84,6 +92,8 @@ export default class MessageBox extends React.PureComponent { if (this.props.message !== nextProps.message && nextProps.message.msg) { this.setState({ text: nextProps.message.msg }); this.component.focus(); + } else if (this.props.replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) { + this.component.focus(); } else if (!nextProps.message) { this.setState({ text: '' }); } @@ -180,6 +190,14 @@ export default class MessageBox extends React.PureComponent { return icons; } + getPermalink = async(message) => { + try { + return await RocketChat.getPermalink(message); + } catch (error) { + return null; + } + } + toggleFilesActions = () => { this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction })); } @@ -259,7 +277,7 @@ export default class MessageBox extends React.PureComponent { this.setState({ showEmojiKeyboard: false }); } - submit(message) { + async submit(message) { this.setState({ text: '' }); this.closeEmoji(); this.stopTrackingMention(); @@ -268,15 +286,32 @@ export default class MessageBox extends React.PureComponent { return; } // if is editing a message - const { editing } = this.props; + const { + editing, replying + } = this.props; + if (editing) { const { _id, rid } = this.props.message; this.props.editRequest({ _id, msg: message, rid }); + } else if (replying) { + const { + username, replyMessage, roomType, closeReply + } = this.props; + const permalink = await this.getPermalink(replyMessage); + let msg = `[ ](${ permalink }) `; + + // if original message wasn't sent by current user and neither from a direct room + if (username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) { + msg += `@${ replyMessage.u.username } `; + } + + msg = `${ msg } ${ message }`; + this.props.onSubmit(msg); + closeReply(); } else { // if is submiting a new message this.props.onSubmit(message); } - this.props.clearInput(); } _getFixedMentions(keyword) { @@ -520,6 +555,14 @@ export default class MessageBox extends React.PureComponent { ); }; + renderReplyPreview = () => { + const { replyMessage, replying, closeReply } = this.props; + if (!replying) { + return null; + } + return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} />; + }; + renderFilesActions = () => { if (!this.state.showFilesAction) { return null; @@ -541,6 +584,7 @@ export default class MessageBox extends React.PureComponent { return ( [ this.renderMentions(), + this.renderReplyPreview(), <View key='messagebox' style={[styles.textArea, this.props.editing && styles.editing]} diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js index 4d779bb421b8f68091ffe4d36f1ce0d250fdfbda..d2dca464bd2890179392c506b547d4996bd5cfc0 100644 --- a/app/containers/message/Markdown.js +++ b/app/containers/message/Markdown.js @@ -32,6 +32,7 @@ export default class Markdown extends React.Component { } let m = formatText(msg); m = emojify(m, { output: 'unicode' }); + m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim(); return ( <MarkdownRenderer rules={{ diff --git a/app/reducers/messages.js b/app/reducers/messages.js index caec481d29b3ddab66a0492e125996bae840dc2d..787efb5497faaa30ac17a9c71c24aff03981c7a5 100644 --- a/app/reducers/messages.js +++ b/app/reducers/messages.js @@ -5,6 +5,7 @@ const initialState = { failure: false, message: {}, actionMessage: {}, + replyMessage: {}, editing: false, showActions: false, showErrorActions: false, @@ -76,6 +77,19 @@ export default function messages(state = initialState, action) { message: {}, editing: false }; + case types.MESSAGES.REPLY_INIT: + return { + ...state, + replyMessage: { + ...action.message, + mention: action.mention + } + }; + case types.MESSAGES.REPLY_CANCEL: + return { + ...state, + replyMessage: {} + }; case types.MESSAGES.SET_INPUT: return { ...state, diff --git a/app/sagas/messages.js b/app/sagas/messages.js index 01e004acd3c8db778ea08f29aa76522d55f9cd31..f959fd18b1c3cbf3e89fa0219919c77ef0c1c06f 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -1,5 +1,5 @@ import { delay } from 'redux-saga'; -import { takeLatest, put, call, select } from 'redux-saga/effects'; +import { takeLatest, put, call } from 'redux-saga/effects'; import { MESSAGES } from '../actions/actionsTypes'; import { @@ -13,7 +13,7 @@ import { toggleStarFailure, togglePinSuccess, togglePinFailure, - setInput + replyInit } from '../actions/messages'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; @@ -99,9 +99,7 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) { yield goRoom({ rid: room.rid, name: username }); } yield delay(500); - const server = yield select(state => state.server.server); - const msg = `[ ](${ server }/direct/${ username }?msg=${ message._id }) `; - yield put(setInput({ msg })); + yield put(replyInit(message, false)); } catch (e) { log('handleReplyBroadcast', e); }