[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_SUCCESS',
'TOGGLE_PIN_FAILURE',
'SET_INPUT',
'CLEAR_INPUT',
'REPLY_INIT',
'REPLY_CANCEL',
'TOGGLE_REACTION_PICKER',
'REPLY_BROADCAST'
]);

View File

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

View File

@ -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() {

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 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]}

View File

@ -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={{

View File

@ -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,

View File

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