diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 454d8cbdc..bca1e4860 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -34,6 +34,8 @@ export const MESSAGES = createRequestTypes('MESSAGES', [ ...defaultTypes, 'ACTIONS_SHOW', 'ACTIONS_HIDE', + 'ERROR_ACTIONS_SHOW', + 'ERROR_ACTIONS_HIDE', 'DELETE_REQUEST', 'DELETE_SUCCESS', 'DELETE_FAILURE', diff --git a/app/actions/messages.js b/app/actions/messages.js index 5d45b2886..29c1b4ca9 100644 --- a/app/actions/messages.js +++ b/app/actions/messages.js @@ -33,6 +33,19 @@ export function actionsHide() { }; } +export function errorActionsShow(actionMessage) { + return { + type: types.MESSAGES.ERROR_ACTIONS_SHOW, + actionMessage + }; +} + +export function errorActionsHide() { + return { + type: types.MESSAGES.ERROR_ACTIONS_HIDE + }; +} + export function deleteRequest(message) { return { type: types.MESSAGES.DELETE_REQUEST, diff --git a/app/constants/messagesStatus.js b/app/constants/messagesStatus.js new file mode 100644 index 000000000..4f945f939 --- /dev/null +++ b/app/constants/messagesStatus.js @@ -0,0 +1,5 @@ +export default { + SENT: 0, + TEMP: 1, + ERROR: 2 +}; diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index 6aadc3870..e84c9aef8 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -140,7 +140,6 @@ export default class MessageBox extends React.Component { } submit(message) { this.component.setNativeProps({ text: '' }); - this.props.clearInput(); this.setState({ text: '' }); requestAnimationFrame(() => { this.props.typing(false); @@ -156,6 +155,7 @@ export default class MessageBox extends React.Component { // if is submiting a new message this.props.onSubmit(message); } + this.props.clearInput(); }); } diff --git a/app/containers/MessageBox/style.js b/app/containers/MessageBox/style.js index 32532515f..d6c521df7 100644 --- a/app/containers/MessageBox/style.js +++ b/app/containers/MessageBox/style.js @@ -18,7 +18,6 @@ export default StyleSheet.create({ textArea: { flexDirection: 'row', alignItems: 'center', - backgroundColor: 'white', flexGrow: 0 }, textBoxInput: { diff --git a/app/containers/MessageErrorActions.js b/app/containers/MessageErrorActions.js new file mode 100644 index 000000000..88bb5487b --- /dev/null +++ b/app/containers/MessageErrorActions.js @@ -0,0 +1,76 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import ActionSheet from 'react-native-actionsheet'; + +import { errorActionsHide } from '../actions/messages'; +import RocketChat from '../lib/rocketchat'; +import realm from '../lib/realm'; + +@connect( + state => ({ + showErrorActions: state.messages.showErrorActions, + actionMessage: state.messages.actionMessage + }), + dispatch => ({ + errorActionsHide: () => dispatch(errorActionsHide()) + }) +) +export default class MessageActions extends React.Component { + static propTypes = { + errorActionsHide: PropTypes.func.isRequired, + showErrorActions: PropTypes.bool.isRequired, + actionMessage: PropTypes.object + }; + + constructor(props) { + super(props); + this.handleActionPress = this.handleActionPress.bind(this); + this.options = ['Cancel', 'Delete', 'Resend']; + this.CANCEL_INDEX = 0; + this.DELETE_INDEX = 1; + this.RESEND_INDEX = 2; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.showErrorActions !== this.props.showErrorActions && nextProps.showErrorActions) { + this.ActionSheet.show(); + } + } + + handleResend = () => RocketChat.resendMessage(this.props.actionMessage._id); + + handleDelete = () => { + realm.write(() => { + const msg = realm.objects('messages').filtered('_id = $0', this.props.actionMessage._id); + realm.delete(msg); + }); + } + + handleActionPress = (actionIndex) => { + switch (actionIndex) { + case this.RESEND_INDEX: + this.handleResend(); + break; + case this.DELETE_INDEX: + this.handleDelete(); + break; + default: + break; + } + this.props.errorActionsHide(); + } + + render() { + return ( + this.ActionSheet = o} + title='Messages actions' + options={this.options} + cancelButtonIndex={this.CANCEL_INDEX} + destructiveButtonIndex={this.DELETE_INDEX} + onPress={this.handleActionPress} + /> + ); + } +} diff --git a/app/containers/message/index.js b/app/containers/message/index.js index f5b35e819..931d5777f 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, StyleSheet, TouchableOpacity, Text } from 'react-native'; +import { View, StyleSheet, TouchableHighlight, Text, TouchableOpacity } from 'react-native'; import { connect } from 'react-redux'; +import Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; -import { actionsShow } from '../../actions/messages'; +import { actionsShow, errorActionsShow } from '../../actions/messages'; import Image from './Image'; import User from './User'; import Avatar from '../Avatar'; @@ -13,6 +14,7 @@ import Video from './Video'; import Markdown from './Markdown'; import Url from './Url'; import Reply from './Reply'; +import messageStatus from '../../constants/messagesStatus'; const styles = StyleSheet.create({ content: { @@ -39,7 +41,8 @@ const styles = StyleSheet.create({ message: state.messages.message, editing: state.messages.editing }), dispatch => ({ - actionsShow: actionMessage => dispatch(actionsShow(actionMessage)) + actionsShow: actionMessage => dispatch(actionsShow(actionMessage)), + errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)) })) export default class Message extends React.Component { static propTypes = { @@ -49,7 +52,8 @@ export default class Message extends React.Component { message: PropTypes.object.isRequired, user: PropTypes.object.isRequired, editing: PropTypes.bool, - actionsShow: PropTypes.func + actionsShow: PropTypes.func, + errorActionsShow: PropTypes.func } onLongPress() { @@ -57,6 +61,11 @@ export default class Message extends React.Component { this.props.actionsShow(JSON.parse(JSON.stringify(item))); } + onErrorPress() { + const { item } = this.props; + this.props.errorActionsShow(JSON.parse(JSON.stringify(item))); + } + isDeleted() { return this.props.item.t === 'rm'; } @@ -65,6 +74,10 @@ export default class Message extends React.Component { return this.props.item.t === 'message_pinned'; } + hasError() { + return this.props.item.status === messageStatus.ERROR; + } + attachments() { if (this.props.item.attachments.length === 0) { return null; @@ -102,13 +115,24 @@ export default class Message extends React.Component { )); } + renderError = () => { + if (!this.hasError()) { + return null; + } + return ( + this.onErrorPress()}> + + + ); + } + render() { const { - item, message, editing + item, message, editing, baseUrl } = this.props; const extraStyle = {}; - if (item.temp) { + if (item.status === messageStatus.TEMP || item.status === messageStatus.ERROR) { extraStyle.opacity = 0.3; } @@ -118,31 +142,38 @@ export default class Message extends React.Component { const accessibilityLabel = `Message from ${ item.alias || item.u.username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`; return ( - this.onLongPress()} - disabled={this.isDeleted()} - style={[styles.message, extraStyle, isEditing ? styles.editing : null]} + disabled={this.isDeleted() || this.hasError()} + underlayColor='#FFFFFF' + activeOpacity={0.3} + style={[styles.message, isEditing ? styles.editing : null]} accessibilityLabel={accessibilityLabel} > - - - - {this.renderMessageContent()} - {this.attachments()} - {this.renderUrl()} + + {this.renderError()} + + + + + {this.renderMessageContent()} + {this.attachments()} + {this.renderUrl()} + + - + ); } } diff --git a/app/lib/realm.js b/app/lib/realm.js index c98d3867d..eaec7ecc8 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -172,7 +172,7 @@ const messagesSchema = { attachments: { type: 'list', objectType: 'attachment' }, urls: { type: 'list', objectType: 'url' }, _updatedAt: { type: 'date', optional: true }, - temp: { type: 'bool', optional: true }, + status: { type: 'int', optional: true }, pinned: { type: 'bool', optional: true }, starred: { type: 'bool', optional: true }, editedBy: 'messagesEditedBy' diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index c7144600a..1cb08a98a 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -6,6 +6,7 @@ import { hashPassword } from 'react-native-meteor/lib/utils'; import RNFetchBlob from 'react-native-fetch-blob'; import reduxStore from './createStore'; import settingsType from '../constants/settings'; +import messagesStatus from '../constants/messagesStatus'; import realm from './realm'; import * as actions from '../actions'; import { someoneTyping } from '../actions/room'; @@ -24,6 +25,7 @@ const call = (method, ...params) => new Promise((resolve, reject) => { }); }); const TOKEN_KEY = 'reactnativemeteor_usertoken'; +const SERVER_TIMEOUT = 30000; const RocketChat = { TOKEN_KEY, @@ -291,7 +293,7 @@ const RocketChat = { }, _buildMessage(message) { const { server } = reduxStore.getState().server; - message.temp = false; + message.status = messagesStatus.SENT; message._server = { id: server }; message.attachments = message.attachments || []; if (message.urls) { @@ -341,7 +343,7 @@ const RocketChat = { msg, ts: new Date(), _updatedAt: new Date(), - temp: true, + status: messagesStatus.TEMP, _server: { id: reduxStore.getState().server.server }, u: { _id: reduxStore.getState().login.user.id || '1', @@ -355,9 +357,29 @@ const RocketChat = { }); return message; }, - sendMessage(rid, msg) { + async _sendMessageCall(message) { + const { _id, rid, msg } = message; + const sendMessageCall = call('sendMessage', { _id, rid, msg }); + const timeoutCall = new Promise(resolve => setTimeout(resolve, SERVER_TIMEOUT, 'timeout')); + const result = await Promise.race([sendMessageCall, timeoutCall]); + if (result === 'timeout') { + realm.write(() => { + message.status = messagesStatus.ERROR; + realm.create('messages', message, true); + }); + } + }, + async sendMessage(rid, msg) { const tempMessage = this.getMessage(rid, msg); - return call('sendMessage', { _id: tempMessage._id, rid, msg }); + return RocketChat._sendMessageCall(tempMessage); + }, + async resendMessage(messageId) { + const message = await realm.objects('messages').filtered('_id = $0', messageId)[0]; + realm.write(() => { + message.status = messagesStatus.TEMP; + realm.create('messages', message, true); + }); + return RocketChat._sendMessageCall(message); }, spotlight(search, usernames) { diff --git a/app/reducers/messages.js b/app/reducers/messages.js index 9061d6f47..7c4022375 100644 --- a/app/reducers/messages.js +++ b/app/reducers/messages.js @@ -7,7 +7,8 @@ const initialState = { actionMessage: {}, editing: false, permalink: '', - showActions: false + showActions: false, + showErrorActions: false }; export default function messages(state = initialState, action) { @@ -40,6 +41,17 @@ export default function messages(state = initialState, action) { ...state, showActions: false }; + case types.MESSAGES.ERROR_ACTIONS_SHOW: + return { + ...state, + showErrorActions: true, + actionMessage: action.actionMessage + }; + case types.MESSAGES.ERROR_ACTIONS_HIDE: + return { + ...state, + showErrorActions: false + }; case types.MESSAGES.EDIT_INIT: return { ...state, diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 85f19bf8a..e8a2647aa 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -12,6 +12,7 @@ import realm from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; import MessageActions from '../../containers/MessageActions'; +import MessageErrorActions from '../../containers/MessageErrorActions'; import MessageBox from '../../containers/MessageBox'; import Typing from '../../containers/Typing'; import KeyboardView from '../../presentation/KeyboardView'; @@ -137,7 +138,7 @@ export default class RoomView extends React.Component { renderItem = ({ item }) => ( {this.renderFooter()} + ); }