[NEW] Reply preview (#374)

* Updated to React Native 0.56

* Reply Preview
This commit is contained in:
Diego Mello 2018-07-20 16:54:46 -03:00 committed by Guilherme Gazzo
parent 077c29503e
commit 8322e7e576
8 changed files with 169 additions and 42 deletions

View File

@ -65,8 +65,8 @@ export const MESSAGES = createRequestTypes('MESSAGES', [
'TOGGLE_PIN_REQUEST', 'TOGGLE_PIN_REQUEST',
'TOGGLE_PIN_SUCCESS', 'TOGGLE_PIN_SUCCESS',
'TOGGLE_PIN_FAILURE', 'TOGGLE_PIN_FAILURE',
'SET_INPUT', 'REPLY_INIT',
'CLEAR_INPUT', 'REPLY_CANCEL',
'TOGGLE_REACTION_PICKER', 'TOGGLE_REACTION_PICKER',
'REPLY_BROADCAST' 'REPLY_BROADCAST'
]); ]);

View File

@ -137,16 +137,17 @@ export function togglePinFailure(err) {
}; };
} }
export function setInput(message) { export function replyInit(message, mention) {
return { return {
type: types.MESSAGES.SET_INPUT, type: types.MESSAGES.REPLY_INIT,
message message,
mention
}; };
} }
export function clearInput() { export function replyCancel() {
return { return {
type: types.MESSAGES.CLEAR_INPUT type: types.MESSAGES.REPLY_CANCEL
}; };
} }

View File

@ -10,9 +10,9 @@ import {
editInit, editInit,
toggleStarRequest, toggleStarRequest,
togglePinRequest, togglePinRequest,
setInput,
actionsHide, actionsHide,
toggleReactionPicker toggleReactionPicker,
replyInit
} from '../actions/messages'; } from '../actions/messages';
import { showToast } from '../utils/info'; import { showToast } from '../utils/info';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -34,8 +34,8 @@ import I18n from '../i18n';
editInit: message => dispatch(editInit(message)), editInit: message => dispatch(editInit(message)),
toggleStarRequest: message => dispatch(toggleStarRequest(message)), toggleStarRequest: message => dispatch(toggleStarRequest(message)),
togglePinRequest: message => dispatch(togglePinRequest(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 { export default class MessageActions extends React.Component {
@ -43,13 +43,13 @@ export default class MessageActions extends React.Component {
actionsHide: PropTypes.func.isRequired, actionsHide: PropTypes.func.isRequired,
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
actionMessage: PropTypes.object, actionMessage: PropTypes.object,
user: PropTypes.object.isRequired, // user: PropTypes.object.isRequired,
deleteRequest: PropTypes.func.isRequired, deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired, editInit: PropTypes.func.isRequired,
toggleStarRequest: PropTypes.func.isRequired, toggleStarRequest: PropTypes.func.isRequired,
togglePinRequest: PropTypes.func.isRequired, togglePinRequest: PropTypes.func.isRequired,
setInput: PropTypes.func.isRequired,
toggleReactionPicker: PropTypes.func.isRequired, toggleReactionPicker: PropTypes.func.isRequired,
replyInit: PropTypes.func.isRequired,
Message_AllowDeleting: PropTypes.bool, Message_AllowDeleting: PropTypes.bool,
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number, Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
Message_AllowEditing: PropTypes.bool, Message_AllowEditing: PropTypes.bool,
@ -248,21 +248,12 @@ export default class MessageActions extends React.Component {
this.props.togglePinRequest(this.props.actionMessage); this.props.togglePinRequest(this.props.actionMessage);
} }
async handleReply() { handleReply() {
const permalink = await this.getPermalink(this.props.actionMessage); this.props.replyInit(this.props.actionMessage, true);
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 });
} }
async handleQuote() { handleQuote() {
const permalink = await this.getPermalink(this.props.actionMessage); this.props.replyInit(this.props.actionMessage, false);
const msg = `[ ](${ permalink }) `;
this.props.setInput({ msg });
} }
handleReaction() { handleReaction() {

View File

@ -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>
);
}
}

View File

@ -9,7 +9,7 @@ import ImagePicker from 'react-native-image-crop-picker';
import { userTyping } from '../../actions/room'; import { userTyping } from '../../actions/room';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import { editRequest, editCancel, clearInput } from '../../actions/messages'; import { editRequest, editCancel, replyCancel } from '../../actions/messages';
import styles from './styles'; import styles from './styles';
import MyIcon from '../icons'; import MyIcon from '../icons';
import database from '../../lib/realm'; import database from '../../lib/realm';
@ -22,6 +22,7 @@ import UploadModal from './UploadModal';
import './EmojiKeyboard'; import './EmojiKeyboard';
import log from '../../utils/log'; import log from '../../utils/log';
import I18n from '../../i18n'; import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview';
const MENTIONS_TRACKING_TYPE_USERS = '@'; const MENTIONS_TRACKING_TYPE_USERS = '@';
const MENTIONS_TRACKING_TYPE_EMOJIS = ':'; const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
@ -39,27 +40,34 @@ const imagePickerConfig = {
}; };
@connect(state => ({ @connect(state => ({
room: state.room, roomType: state.room.t,
message: state.messages.message, message: state.messages.message,
replyMessage: state.messages.replyMessage,
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg,
editing: state.messages.editing, 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 => ({ }), dispatch => ({
editCancel: () => dispatch(editCancel()), editCancel: () => dispatch(editCancel()),
editRequest: message => dispatch(editRequest(message)), editRequest: message => dispatch(editRequest(message)),
typing: status => dispatch(userTyping(status)), typing: status => dispatch(userTyping(status)),
clearInput: () => dispatch(clearInput()) closeReply: () => dispatch(replyCancel())
})) }))
export default class MessageBox extends React.PureComponent { export default class MessageBox extends React.PureComponent {
static propTypes = { static propTypes = {
onSubmit: PropTypes.func.isRequired,
rid: PropTypes.string.isRequired, rid: PropTypes.string.isRequired,
editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
message: PropTypes.object, message: PropTypes.object,
replyMessage: PropTypes.object,
replying: PropTypes.bool,
editing: 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, typing: PropTypes.func,
clearInput: PropTypes.func closeReply: PropTypes.func
} }
constructor(props) { constructor(props) {
@ -84,6 +92,8 @@ export default class MessageBox extends React.PureComponent {
if (this.props.message !== nextProps.message && nextProps.message.msg) { if (this.props.message !== nextProps.message && nextProps.message.msg) {
this.setState({ text: nextProps.message.msg }); this.setState({ text: nextProps.message.msg });
this.component.focus(); this.component.focus();
} else if (this.props.replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
this.component.focus();
} else if (!nextProps.message) { } else if (!nextProps.message) {
this.setState({ text: '' }); this.setState({ text: '' });
} }
@ -180,6 +190,14 @@ export default class MessageBox extends React.PureComponent {
return icons; return icons;
} }
getPermalink = async(message) => {
try {
return await RocketChat.getPermalink(message);
} catch (error) {
return null;
}
}
toggleFilesActions = () => { toggleFilesActions = () => {
this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction })); this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction }));
} }
@ -259,7 +277,7 @@ export default class MessageBox extends React.PureComponent {
this.setState({ showEmojiKeyboard: false }); this.setState({ showEmojiKeyboard: false });
} }
submit(message) { async submit(message) {
this.setState({ text: '' }); this.setState({ text: '' });
this.closeEmoji(); this.closeEmoji();
this.stopTrackingMention(); this.stopTrackingMention();
@ -268,15 +286,32 @@ export default class MessageBox extends React.PureComponent {
return; return;
} }
// if is editing a message // if is editing a message
const { editing } = this.props; const {
editing, replying
} = this.props;
if (editing) { if (editing) {
const { _id, rid } = this.props.message; const { _id, rid } = this.props.message;
this.props.editRequest({ _id, msg: message, rid }); 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 { } else {
// if is submiting a new message // if is submiting a new message
this.props.onSubmit(message); this.props.onSubmit(message);
} }
this.props.clearInput();
} }
_getFixedMentions(keyword) { _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 = () => { renderFilesActions = () => {
if (!this.state.showFilesAction) { if (!this.state.showFilesAction) {
return null; return null;
@ -541,6 +584,7 @@ export default class MessageBox extends React.PureComponent {
return ( return (
[ [
this.renderMentions(), this.renderMentions(),
this.renderReplyPreview(),
<View <View
key='messagebox' key='messagebox'
style={[styles.textArea, this.props.editing && styles.editing]} style={[styles.textArea, this.props.editing && styles.editing]}

View File

@ -32,6 +32,7 @@ export default class Markdown extends React.Component {
} }
let m = formatText(msg); let m = formatText(msg);
m = emojify(m, { output: 'unicode' }); m = emojify(m, { output: 'unicode' });
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
return ( return (
<MarkdownRenderer <MarkdownRenderer
rules={{ rules={{

View File

@ -5,6 +5,7 @@ const initialState = {
failure: false, failure: false,
message: {}, message: {},
actionMessage: {}, actionMessage: {},
replyMessage: {},
editing: false, editing: false,
showActions: false, showActions: false,
showErrorActions: false, showErrorActions: false,
@ -76,6 +77,19 @@ export default function messages(state = initialState, action) {
message: {}, message: {},
editing: false 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: case types.MESSAGES.SET_INPUT:
return { return {
...state, ...state,

View File

@ -1,5 +1,5 @@
import { delay } from 'redux-saga'; 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 { MESSAGES } from '../actions/actionsTypes';
import { import {
@ -13,7 +13,7 @@ import {
toggleStarFailure, toggleStarFailure,
togglePinSuccess, togglePinSuccess,
togglePinFailure, togglePinFailure,
setInput replyInit
} from '../actions/messages'; } from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
@ -99,9 +99,7 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
yield goRoom({ rid: room.rid, name: username }); yield goRoom({ rid: room.rid, name: username });
} }
yield delay(500); yield delay(500);
const server = yield select(state => state.server.server); yield put(replyInit(message, false));
const msg = `[ ](${ server }/direct/${ username }?msg=${ message._id }) `;
yield put(setInput({ msg }));
} catch (e) { } catch (e) {
log('handleReplyBroadcast', e); log('handleReplyBroadcast', e);
} }