diff --git a/src/lib/Network.js b/src/lib/Network.js
index 7ac7e4914fec2..45ee45e545417 100644
--- a/src/lib/Network.js
+++ b/src/lib/Network.js
@@ -34,7 +34,13 @@ function request(command, data, type = 'post') {
console.info('[API] Error', responseData);
})
// eslint-disable-next-line no-unused-vars
- .catch(() => isAppOffline = true);
+ .catch(() => {
+ isAppOffline = true;
+
+ // Throw a new error to prevent any other `then()` in the promise chain from being triggered (until another
+ // catch() happens
+ throw new Error('API is offline');
+ });
}
// Holds a queue of all the write requests that need to happen
@@ -61,30 +67,35 @@ function delayedWrite(command, data) {
/**
* Process the write queue by looping through the queue and attempting to make the requests
*/
-// function processWriteQueue() {
-// if (isAppOffline) {
-// // Make a simple request to see if we're online again
-// request('Get', null, 'get')
-// .then(() => isAppOffline = false);
-// return;
-// }
-//
-// if (delayedWriteQueue.length === 0) {
-// return;
-// }
-//
-// _.each(delayedWriteQueue, (delayedWriteRequest) => {
-// request(delayedWriteRequest.command, delayedWriteRequest.data)
-// .then(delayedWriteRequest.callback)
-// .catch(() => {
-// // If the request failed, we need to put the request object back into the queue
-// delayedWriteQueue.push(delayedWriteRequest);
-// });
-// });
-// }
+function processWriteQueue() {
+ if (isAppOffline) {
+ // Make a simple request to see if we're online again
+ request('Get', null)
+ .then(() => isAppOffline = false);
+ return;
+ }
+
+ if (delayedWriteQueue.length === 0) {
+ return;
+ }
+
+ for (let i = 0; i < delayedWriteQueue.length; i++) {
+ // Take the request object out of the queue and make the request
+ const delayedWriteRequest = delayedWriteQueue.shift();
+
+ request(delayedWriteRequest.command, delayedWriteRequest.data)
+ .then(delayedWriteRequest.callback)
+ .catch(() => {
+ // If the request failed, we need to put the request object back into the queue
+ delayedWriteQueue.push(delayedWriteRequest);
+ });
+ }
+}
-// TODO: Figure out setInterval
// Process our write queue very often
-// setInterval(processWriteQueue, 1000);
+setInterval(processWriteQueue, 1000);
-export {request, delayedWrite};
+export {
+ request,
+ delayedWrite,
+};
diff --git a/src/lib/Pusher/pusher.js b/src/lib/Pusher/pusher.js
index d257fdf0b4e90..e09145bb98025 100644
--- a/src/lib/Pusher/pusher.js
+++ b/src/lib/Pusher/pusher.js
@@ -24,7 +24,8 @@ function init(appKey, params) {
});
// If we want to pass params in our requests to api.php we'll need to add it to socket.config.auth.params
- // as per the documentation (https://pusher.com/docs/channels/using_channels/connection#channels-options-parameter).
+ // as per the documentation
+ // (https://pusher.com/docs/channels/using_channels/connection#channels-options-parameter).
// Any param mentioned here will show up in $_REQUEST when we call "Push_Authenticate". Params passed here need
// to pass our inputRules to show up in the request.
if (params) {
@@ -92,8 +93,9 @@ function bindEventToChannel(channel, eventName, eventCallback = () => {}, isChun
return;
}
- // If we are chunking the requests, we need to construct a rolling list of all packets that have come through Pusher.
- // If we've completed one of these full packets, we'll combine the data and act on the event that it's assigned to.
+ // If we are chunking the requests, we need to construct a rolling list of all packets that have come through
+ // Pusher. If we've completed one of these full packets, we'll combine the data and act on the event that it's
+ // assigned to.
// If we haven't seen this eventID yet, initialize it into our rolling list of packets.
if (!chunkedDataEvents[eventData.id]) {
@@ -109,13 +111,17 @@ function bindEventToChannel(channel, eventName, eventCallback = () => {}, isChun
chunkedEvent.receivedFinal = true;
}
- // Only call the event callback if we've received the last packet and we don't have any holes in the complete packet.
+ // Only call the event callback if we've received the last packet and we don't have any holes in the complete
+ // packet.
if (chunkedEvent.receivedFinal && chunkedEvent.chunks.length === Object.keys(chunkedEvent.chunks).length) {
eventCallback(JSON.parse(chunkedEvent.chunks.join('')));
try {
eventCallback(JSON.parse(chunkedEvent.chunks.join('')));
} catch (err) {
- console.error('[Pusher] Unable to parse chunked JSON response from Pusher', 0, {error: err, eventData: chunkedEvent.chunks.join('')});
+ console.error('[Pusher] Unable to parse chunked JSON response from Pusher', 0, {
+ error: err,
+ eventData: chunkedEvent.chunks.join('')
+ });
}
delete chunkedDataEvents[eventData.id];
@@ -131,7 +137,8 @@ function bindEventToChannel(channel, eventName, eventCallback = () => {}, isChun
* @param {String} channelName
* @param {String} eventName
* @param {Function} [eventCallback]
- * @param {Boolean} [isChunked] This parameters tells us whether or not we expect the result to come in individual pieces/chunks (because it exceeds
+ * @param {Boolean} [isChunked] This parameters tells us whether or not we expect the result to come in individual
+ * pieces/chunks (because it exceeds
* the 10kB limit that pusher has).
*
* @return {Promise}
@@ -142,7 +149,8 @@ function subscribe(channelName, eventName, eventCallback = () => {}, isChunked =
return new Promise((resolve, reject) => {
// We cannot call subscribe() before init(). Prevent any attempt to do this on dev.
if (!socket) {
- throw new Error('[Pusher] instance not found. Pusher.subscribe() most likely has been called before Pusher.init()');
+ throw new Error(`[Pusher] instance not found. Pusher.subscribe()
+ most likely has been called before Pusher.init()`);
}
console.debug('[Pusher] Attempting to subscribe to channel', true, {channelName, eventName});
@@ -157,7 +165,10 @@ function subscribe(channelName, eventName, eventCallback = () => {}, isChunked =
channel.bind('pusher:subscription_error', (status) => {
if (status === 403) {
- console.debug('[Pusher] Issue authenticating with Pusher during subscribe attempt.', 0, {channelName, status});
+ console.debug('[Pusher] Issue authenticating with Pusher during subscribe attempt.', 0, {
+ channelName,
+ status
+ });
}
reject(status);
@@ -200,7 +211,8 @@ function unsubscribe(channelName, eventName = '') {
const channel = getChannel(channelName);
if (!channel) {
- console.debug('[Pusher] Attempted to unsubscribe or unbind from a channel, but Pusher-JS has no knowledge of it', 0, {channelName, eventName});
+ console.debug(`[Pusher] Attempted to unsubscribe or unbind from a channel,
+ but Pusher-JS has no knowledge of it`, 0, {channelName, eventName});
return;
}
@@ -210,7 +222,8 @@ function unsubscribe(channelName, eventName = '') {
} else {
if (!channel.subscribed) {
// eslint-disable-next-line no-console
- console.warn('[Pusher] Attempted to unsubscribe from channel, but we are not subscribed to begin with', 0, {channelName});
+ console.warn(`[Pusher] Attempted to unsubscribe from channel,
+ but we are not subscribed to begin with`, 0, {channelName});
return;
}
diff --git a/src/page/HomePage/Report/ReportHistoryCompose.js b/src/page/HomePage/Report/ReportHistoryCompose.js
new file mode 100644
index 0000000000000..8233b357744e2
--- /dev/null
+++ b/src/page/HomePage/Report/ReportHistoryCompose.js
@@ -0,0 +1,102 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {View, TextInput, Button} from 'react-native';
+import styles from '../../../style/StyleSheet';
+
+const propTypes = {
+ // A method to call when the form is submitted
+ onSubmit: PropTypes.func.isRequired,
+};
+
+class ReportHistoryCompose extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateComment = this.updateComment.bind(this);
+ this.submitForm = this.submitForm.bind(this);
+ this.triggerSubmitShortcut = this.triggerSubmitShortcut.bind(this);
+
+ this.state = {
+ comment: '',
+ };
+ }
+
+ componentDidMount() {
+ this.textInput.focus();
+ }
+
+ componentDidUpdate() {
+ this.textInput.focus();
+ }
+
+ /**
+ * Update the value of the comment input in the state
+ *
+ * @param {string} newComment
+ */
+ updateComment(newComment) {
+ this.setState({
+ comment: newComment,
+ });
+ }
+
+ /**
+ * Listens for the keyboard shortcut and submits
+ * the form when we have enter
+ *
+ * @param {Object} e
+ */
+ triggerSubmitShortcut(e) {
+ if (e && e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ this.submitForm();
+ }
+ }
+
+ /**
+ * Add a new comment to this chat
+ *
+ * @param {SyntheticEvent} [e]
+ */
+ submitForm(e) {
+ if (e) {
+ e.preventDefault();
+ }
+
+ // Don't submit empty commentes
+ // @TODO show an error in the UI
+ if (!this.state.comment) {
+ return;
+ }
+
+ this.props.onSubmit(this.state.comment);
+ this.setState({
+ comment: '',
+ });
+ }
+
+ render() {
+ return (
+
+ this.textInput = el}
+ multiline
+ textAlignVertical
+ numberOfLines={3}
+ minHeight={60}
+ maxHeight={60}
+ onChangeText={this.updateComment}
+ onKeyPress={this.triggerSubmitShortcut}
+ style={[styles.textInput]}
+ value={this.state.comment}
+ />
+
+
+
+
+ );
+ }
+}
+ReportHistoryCompose.propTypes = propTypes;
+
+export default ReportHistoryCompose;
diff --git a/src/page/HomePage/Report/ReportHistoryItem.js b/src/page/HomePage/Report/ReportHistoryItem.js
index 7fb9e0705a0c1..ed3cfab39393d 100644
--- a/src/page/HomePage/Report/ReportHistoryItem.js
+++ b/src/page/HomePage/Report/ReportHistoryItem.js
@@ -1,5 +1,5 @@
import React from 'react';
-import {Text, View} from 'react-native';
+import {ActivityIndicator, View} from 'react-native';
import PropTypes from 'prop-types';
import ReportHistoryItemSingle from './ReportHistoryItemSingle';
import ReportHistoryPropsTypes from './ReportHistoryPropsTypes';
@@ -21,7 +21,7 @@ class ReportHistoryItem extends React.Component {
{!this.props.displayAsGroup && }
{this.props.displayAsGroup && }
- {this.props.historyItem.tempGuid && pending...}
+ {this.props.historyItem.tempGuid && }
);
}
diff --git a/src/page/HomePage/Report/ReportHistoryView.js b/src/page/HomePage/Report/ReportHistoryView.js
index 2a5f1946dedd9..1e9e79378ed99 100644
--- a/src/page/HomePage/Report/ReportHistoryView.js
+++ b/src/page/HomePage/Report/ReportHistoryView.js
@@ -1,5 +1,5 @@
import React from 'react';
-import {Text, FlatList} from 'react-native';
+import {Text, VirtualizedList} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import lodashGet from 'lodash.get';
@@ -18,6 +18,10 @@ const propTypes = {
};
class ReportHistoryView extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
componentDidMount() {
this.bindToStore();
}
@@ -54,10 +58,6 @@ class ReportHistoryView extends React.Component {
loaderParams: [this.props.reportID],
}
}, this);
-
- if (this.reportHistoryList) {
- this.reportHistoryList.scrollToEnd();
- }
}
/**
@@ -97,9 +97,13 @@ class ReportHistoryView extends React.Component {
}
return (
- this.reportHistoryList = el}
- data={filteredHistory}
+ data={filteredHistory.reverse()}
+ getItemCount={() => filteredHistory.length}
+ getItem={(data, index) => filteredHistory[index]}
+ initialNumToRender="10"
+ inverted
renderItem={({index, item}) => (
+ addHistoryItem(this.props.match.params.reportID, text)} />
);
diff --git a/src/store/actions/ReportActions.js b/src/store/actions/ReportActions.js
index b0714398b841a..c38eefc8525c9 100644
--- a/src/store/actions/ReportActions.js
+++ b/src/store/actions/ReportActions.js
@@ -49,7 +49,9 @@ function updateReportWithNewAction(reportID, reportAction) {
}
return reportHistory;
})
- .then(reportHistory => Store.set(`${STOREKEYS.REPORT}_${reportID}_history`, reportHistory.sort(sortReportActions)));
+ .then((reportHistory) => {
+ Store.set(`${STOREKEYS.REPORT}_${reportID}_history`, reportHistory.sort(sortReportActions));
+ });
}
/**
@@ -83,9 +85,12 @@ function hasUnreadHistoryItems(accountID, report) {
*/
function initPusher() {
return Store.get(STOREKEYS.SESSION, 'accountID')
- .then(accountID => pusher.subscribe(`private-user-accountID-${accountID}`, 'reportComment', (pushJSON) => {
- updateReportWithNewAction(pushJSON.reportID, pushJSON.reportAction);
- }));
+ .then((accountID) => {
+ const pusherChannelName = `private-user-accountID-${accountID}`;
+ pusher.subscribe(pusherChannelName, 'reportComment', (pushJSON) => {
+ updateReportWithNewAction(pushJSON.reportID, pushJSON.reportAction);
+ });
+ });
}
/**
@@ -169,10 +174,10 @@ function fetchHistory(reportID) {
* Add a history item to a report
*
* @param {string} reportID
- * @param {string} commentText
+ * @param {string} reportComment
* @returns {Promise}
*/
-function addHistoryItem(reportID, commentText) {
+function addHistoryItem(reportID, reportComment) {
const messageParser = new ExpensiMark();
const guid = Guid();
const historyKey = `${STOREKEYS.REPORT}_${reportID}_history`;
@@ -204,14 +209,14 @@ function addHistoryItem(reportID, commentText) {
}
],
automatic: false,
- sequenceNumber: highestSequenceNumber++,
+ sequenceNumber: ++highestSequenceNumber,
avatar: personalDetails.avatarURL,
- timestamp: moment.unix(),
+ timestamp: moment().unix(),
message: [
{
type: 'COMMENT',
- html: messageParser.replace(commentText),
- text: commentText,
+ html: messageParser.replace(reportComment),
+ text: reportComment,
}
],
isFirstItem: false,
@@ -221,7 +226,7 @@ function addHistoryItem(reportID, commentText) {
})
.then(() => delayedWrite('Report_AddComment', {
reportID,
- reportComment: commentText,
+ reportComment,
}));
}
diff --git a/src/style/StyleSheet.js b/src/style/StyleSheet.js
index 9eb2708273565..341febfa75c6e 100644
--- a/src/style/StyleSheet.js
+++ b/src/style/StyleSheet.js
@@ -9,6 +9,9 @@ const styles = {
mt2: {
marginTop: 20,
},
+ mt1: {
+ marginTop: 10,
+ },
p1: {
padding: 10,
},
@@ -101,6 +104,12 @@ const styles = {
reportHistoryItemUserName: {
fontWeight: 'bold',
},
+ textInput: {
+ borderColor: '#7d8b8f',
+ borderRadius: 5,
+ borderWidth: 1,
+ padding: 10,
+ },
};
export default styles;