From 8322e7e57632841c8d3be787081e819acb6935b4 Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Fri, 20 Jul 2018 16:54:46 -0300
Subject: [PATCH] [NEW] Reply preview (#374)

* Updated to React Native 0.56

* Reply Preview
---
 app/actions/actionsTypes.js               |  4 +-
 app/actions/messages.js                   | 11 ++--
 app/containers/MessageActions.js          | 29 +++------
 app/containers/MessageBox/ReplyPreview.js | 78 +++++++++++++++++++++++
 app/containers/MessageBox/index.js        | 66 +++++++++++++++----
 app/containers/message/Markdown.js        |  1 +
 app/reducers/messages.js                  | 14 ++++
 app/sagas/messages.js                     |  8 +--
 8 files changed, 169 insertions(+), 42 deletions(-)
 create mode 100644 app/containers/MessageBox/ReplyPreview.js

diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 56fb4f21b..961b88eee 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 b7b3ee690..1b8451c7b 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 ccb25a79d..7eec64339 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 000000000..d71816c40
--- /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 c16cc3f81..63eb3bb12 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 4d779bb42..d2dca464b 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 caec481d2..787efb549 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 01e004acd..f959fd18b 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);
 	}
-- 
GitLab