diff --git a/android/app/build.gradle b/android/app/build.gradle
index 678c40309eb..e1bfcab211b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -148,8 +148,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001002903
- versionName "1.0.29-3"
+ versionCode 1001003001
+ versionName "1.0.30-1"
}
splits {
abi {
diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist
index c359b2f37c5..5e856bc6679 100644
--- a/ios/ExpensifyCash/Info.plist
+++ b/ios/ExpensifyCash/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0.29
+ 1.0.30
CFBundleSignature
????
CFBundleURLTypes
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.0.29.3
+ 1.0.30.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist
index c16db31b438..67c1c8a7a78 100644
--- a/ios/ExpensifyCashTests/Info.plist
+++ b/ios/ExpensifyCashTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.0.29
+ 1.0.30
CFBundleSignature
????
CFBundleVersion
- 1.0.29.3
+ 1.0.30.1
diff --git a/package-lock.json b/package-lock.json
index 992c0db7754..3e0c2963072 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.29-3",
+ "version": "1.0.30-1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 889046cfb9b..96558898515 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.29-3",
+ "version": "1.0.30-1",
"author": "Expensify, Inc.",
"homepage": "https://expensify.cash",
"description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.js b/src/CONST.js
index 95791869e8f..8f0c8b21e0c 100644
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -32,6 +32,10 @@ const CONST = {
IOU: 'IOU',
},
},
+ TYPE: {
+ CHAT: 'chat',
+ IOU: 'iou',
+ },
},
MODAL: {
MODAL_TYPE: {
diff --git a/src/ROUTES.js b/src/ROUTES.js
index a6c60a4809d..404b8c4cfdd 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -1,5 +1,5 @@
import lodashGet from 'lodash/get';
-import {wrapWithForwardSlash} from './libs/Url';
+import {addTrailingForwardSlash} from './libs/Url';
/**
* This is a file containing constants for all of the routes we want to be able to go to
@@ -43,13 +43,14 @@ export default {
* @returns {Object}
*/
parseReportRouteParams: (route) => {
- if (!route.startsWith(wrapWithForwardSlash(REPORT))) {
+ if (!route.startsWith(addTrailingForwardSlash(REPORT))) {
return {};
}
const pathSegments = route.split('/');
return {
reportID: lodashGet(pathSegments, 1),
+ isParticipantsRoute: Boolean(lodashGet(pathSegments, 2)),
};
},
};
diff --git a/src/components/ReportActionItemIOUPreview.js b/src/components/ReportActionItemIOUPreview.js
new file mode 100644
index 00000000000..aa3d3a0778a
--- /dev/null
+++ b/src/components/ReportActionItemIOUPreview.js
@@ -0,0 +1,140 @@
+import React from 'react';
+import {View, TouchableOpacity} from 'react-native';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
+import lodashGet from 'lodash/get';
+import Str from 'expensify-common/lib/str';
+import ONYXKEYS from '../ONYXKEYS';
+import ReportActionItemIOUQuote from './ReportActionItemIOUQuote';
+import ReportActionPropTypes from '../pages/home/report/ReportActionPropTypes';
+import Text from './Text';
+import MultipleAvatars from './MultipleAvatars';
+import styles from '../styles/styles';
+
+const propTypes = {
+ // All the data of the action
+ action: PropTypes.shape(ReportActionPropTypes).isRequired,
+
+ // Is this the most recent IOU Action?
+ isMostRecentIOUReportAction: PropTypes.bool.isRequired,
+
+ // Whether there is an outstanding amount in IOU
+ hasOutstandingIOU: PropTypes.bool.isRequired,
+
+ /* --- Onyx Props --- */
+ // Active IOU Report for current report
+ iou: PropTypes.shape({
+ // Email address of the manager in this iou report
+ managerEmail: PropTypes.string,
+
+ // Email address of the creator of this iou report
+ ownerEmail: PropTypes.string,
+
+ // Outstanding amount of this transaction
+ cachedTotal: PropTypes.string,
+ }),
+
+ // All of the personal details for everyone
+ personalDetails: PropTypes.objectOf(PropTypes.shape({
+
+ // This is either the user's full name, or their login if full name is an empty string
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+
+ // Session info for the currently logged in user.
+ session: PropTypes.shape({
+ // Currently logged in user email
+ email: PropTypes.string,
+ }).isRequired,
+};
+
+const defaultProps = {
+ iou: {},
+};
+
+const ReportActionItemIOUPreview = ({
+ action,
+ isMostRecentIOUReportAction,
+ hasOutstandingIOU,
+ iou,
+ personalDetails,
+ session,
+}) => {
+ const managerName = lodashGet(
+ personalDetails,
+ [iou.managerEmail, 'displayName'],
+ iou.managerEmail ? Str.removeSMSDomain(iou.managerEmail) : '',
+ );
+ const ownerName = lodashGet(
+ personalDetails,
+ [iou.ownerEmail, 'displayName'],
+ iou.ownerEmail ? Str.removeSMSDomain(iou.ownerEmail) : '',
+ );
+ const managerAvatar = lodashGet(personalDetails, [iou.managerEmail, 'avatar'], '');
+ const ownerAvatar = lodashGet(personalDetails, [iou.ownerEmail, 'avatar'], '');
+ const sessionEmail = lodashGet(session, 'email', null);
+ const cachedTotal = iou.cachedTotal ? iou.cachedTotal.replace(/[()]/g, '') : '';
+
+ // Pay button should be visible to manager person in the report
+ // Check if the currently logged in user is the manager.
+ const isCurrentUserManager = iou.managerEmail === sessionEmail;
+
+ return (
+
+
+ {isMostRecentIOUReportAction
+ && hasOutstandingIOU
+ && !_.isEmpty(iou) && (
+
+
+
+ {cachedTotal}
+
+ {managerName}
+ {' owes '}
+ {ownerName}
+
+
+
+
+
+
+ {isCurrentUserManager && (
+
+
+ Pay
+
+
+ )}
+
+ )}
+
+ );
+};
+
+ReportActionItemIOUPreview.propTypes = propTypes;
+ReportActionItemIOUPreview.defaultProps = defaultProps;
+ReportActionItemIOUPreview.displayName = 'ReportActionItemIOUPreview';
+
+export default withOnyx({
+ iou: {
+ key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReportID}`,
+ },
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(ReportActionItemIOUPreview);
diff --git a/src/components/ReportActionItemIOUQuote.js b/src/components/ReportActionItemIOUQuote.js
new file mode 100644
index 00000000000..bf56ed9d243
--- /dev/null
+++ b/src/components/ReportActionItemIOUQuote.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import styles from '../styles/styles';
+import ReportActionPropTypes from '../pages/home/report/ReportActionPropTypes';
+import RenderHTML from './RenderHTML';
+
+const propTypes = {
+ // All the data of the action
+ action: PropTypes.shape(ReportActionPropTypes).isRequired,
+};
+
+const ReportActionItemIOUQuote = ({action}) => (
+
+ {_.map(action.message, (fragment, index) => {
+ const viewDetails = '
View Details';
+ const html = `${fragment.text}${viewDetails}
`;
+ return (
+
+ );
+ })}
+
+);
+
+ReportActionItemIOUQuote.propTypes = propTypes;
+ReportActionItemIOUQuote.displayName = 'ReportActionItemIOUQuote';
+
+export default ReportActionItemIOUQuote;
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index d0c8b1988b9..d136fdf2cdf 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -10,7 +10,7 @@ import CONST from '../../../CONST';
import compose from '../../compose';
import {
subscribeToReportCommentEvents,
- fetchAll as fetchAllReports,
+ fetchAllReports,
} from '../../actions/Report';
import * as PersonalDetails from '../../actions/PersonalDetails';
import * as Pusher from '../../Pusher/pusher';
@@ -119,7 +119,7 @@ class AuthScreens extends React.Component {
PersonalDetails.fetch();
User.getUserDetails();
User.getBetas();
- fetchAllReports(true, true);
+ fetchAllReports(true, true, true);
fetchCountryCodeByRequestIP();
UnreadIndicatorUpdater.listenForReportChanges();
diff --git a/src/libs/Navigation/CustomActions.js b/src/libs/Navigation/CustomActions.js
index 74570a88ede..d29cbed73b9 100644
--- a/src/libs/Navigation/CustomActions.js
+++ b/src/libs/Navigation/CustomActions.js
@@ -14,8 +14,15 @@ import {CommonActions} from '@react-navigation/native';
*/
function pushDrawerRoute(screenName, params) {
return (state) => {
+ // Non Drawer navigators have routes and not history so we'll fallback to navigate() in the case where we are
+ // unable to push a new screen onto the history stack e.g. navigating to a ReportScreen via a modal screen.
+ // Note: One downside of this is that the history will be reset.
+ if (state.type !== 'drawer') {
+ return CommonActions.navigate(screenName, params);
+ }
+
const screenRoute = {type: 'route', name: screenName};
- const history = [...state.history].map(() => screenRoute);
+ const history = [...(state.history || [])].map(() => screenRoute);
history.push(screenRoute);
return CommonActions.reset({
...state,
diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js
index 30cce16a51a..28dbfe330da 100644
--- a/src/libs/Navigation/Navigation.js
+++ b/src/libs/Navigation/Navigation.js
@@ -48,8 +48,10 @@ function navigate(route = ROUTES.HOME) {
return;
}
- const {reportID} = ROUTES.parseReportRouteParams(route);
- if (reportID) {
+ // Navigate to the ReportScreen with a custom action so that we can preserve the history. We're looking to see if we
+ // have a participants route since those should go through linkTo() as they open a different screen.
+ const {reportID, isParticipantsRoute} = ROUTES.parseReportRouteParams(route);
+ if (reportID && !isParticipantsRoute) {
navigationRef.current.dispatch(CustomActions.pushDrawerRoute(SCREENS.REPORT, {reportID}));
return;
}
diff --git a/src/libs/Url.js b/src/libs/Url.js
index 004e9c5fe17..e166c127a83 100644
--- a/src/libs/Url.js
+++ b/src/libs/Url.js
@@ -10,23 +10,7 @@ function addTrailingForwardSlash(url) {
return url;
}
-/**
- * Add / to the beginning and end of any URL if not present
- * @param {String} url
- * @returns {String}
- */
-function wrapWithForwardSlash(url) {
- const newUrl = addTrailingForwardSlash(url);
- if (newUrl.startsWith('/')) {
- return newUrl;
- }
-
- return `/${newUrl}`;
-}
-
-
export {
// eslint-disable-next-line import/prefer-default-export
addTrailingForwardSlash,
- wrapWithForwardSlash,
};
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index cd3e7cfbe6f..d4769920c34 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -44,6 +44,16 @@ Onyx.connect({
callback: val => myPersonalDetails = val,
});
+const allReports = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ callback: (val) => {
+ if (val && val.reportID) {
+ allReports[val.reportID] = val;
+ }
+ },
+});
+
const typingWatchTimers = {};
// Keeps track of the max sequence number for each report
@@ -195,82 +205,69 @@ function getSimplifiedIOUReport(reportData, chatReportID) {
}
/**
- * Fetches the updated data for an IOU Report and updates the IOU collection in Onyx
+ * Given IOU and chat report ID fetches most recent IOU data from API.
*
- * @param {Object} chatReport
- * @param {Object[]} chatReport.reportActionList
- * @param {Number} chatReport.reportID
- * @return {Promise}
+ * @param {Number} iouReportID
+ * @param {Number} chatReportID
+ * @returns {Promise}
*/
-function updateIOUReportData(chatReport) {
- const reportActionList = chatReport.reportActionList || [];
- const containsIOUAction = _.any(reportActionList,
- reportAction => reportAction.action === CONST.REPORT.ACTIONS.TYPE.IOU);
-
- // If there aren't any IOU actions, we don't need to fetch any additional data
- if (!containsIOUAction) {
- return;
- }
-
- // If we don't have one participant (other than the current user), this is not an IOU
- const participants = getParticipantEmailsFromReport(chatReport);
- if (participants.length !== 1) {
- Log.alert('[Report] Report with IOU action has more than 2 participants', true, {
- reportID: chatReport.reportID,
- participants,
- });
- return;
- }
-
- // Since the Chat and the IOU are different reports with different reportIDs, and GetIOUReport only returns the
- // IOU's reportID, keep track of the IOU's reportID so we can use it to get the IOUReport data via `GetReportStuff`.
- // Note: GetIOUReport does not return IOU reports that have been settled.
- let iouReportID = 0;
- return API.GetIOUReport({
- debtorEmail: participants[0],
- }).then((response) => {
- iouReportID = response.reportID || 0;
- if (response.jsonCode !== 200) {
- throw new Error(response.message);
- } else if (iouReportID === 0) {
- // If there is no IOU report for this user then we will assume it has been paid and do nothing here.
- // All reports are initialized with hasOutstandingIOU: false. Since the IOU report we were looking for has
- // been settled then there's nothing more to do.
- console.debug('GetIOUReport returned a reportID of 0, not fetching IOU report data');
- return;
- }
-
- return API.Get({
- returnValueList: 'reportStuff',
- reportIDList: iouReportID,
- shouldLoadOptionalKeys: true,
- includePinnedReports: true,
- });
+function fetchIOUReport(iouReportID, chatReportID) {
+ return API.Get({
+ returnValueList: 'reportStuff',
+ reportIDList: iouReportID,
+ shouldLoadOptionalKeys: true,
+ includePinnedReports: true,
}).then((response) => {
if (!response) {
return;
}
-
if (response.jsonCode !== 200) {
- throw new Error(response.message);
+ console.error(response.message);
+ return;
}
-
const iouReportData = response.reports[iouReportID];
if (!iouReportData) {
- throw new Error(`No iouReportData found for reportID ${iouReportID}`);
+ console.error(`No iouReportData found for reportID ${iouReportID}`);
+ return;
}
- return getSimplifiedIOUReport(iouReportData, chatReport.reportID);
+ return getSimplifiedIOUReport(iouReportData, chatReportID);
}).catch((error) => {
console.debug(`[Report] Failed to populate IOU Collection: ${error.message}`);
});
}
+/**
+ * Given debtorEmail finds active IOU report ID via GetIOUReport API call
+ *
+ * @param {String} debtorEmail
+ * @returns {Promise}
+ */
+function fetchIOUReportID(debtorEmail) {
+ return API.GetIOUReport({
+ debtorEmail,
+ }).then((response) => {
+ const iouReportID = response.reportID || 0;
+ if (response.jsonCode !== 200) {
+ console.error(response.message);
+ return;
+ }
+ if (iouReportID === 0) {
+ // If there is no IOU report for this user then we will assume it has been paid and do nothing here.
+ // All reports are initialized with hasOutstandingIOU: false. Since the IOU report we were looking for has
+ // been settled then there's nothing more to do.
+ console.debug('GetIOUReport returned a reportID of 0, not fetching IOU report data');
+ return;
+ }
+ return iouReportID;
+ });
+}
+
/**
* Fetches chat reports when provided a list of
* chat report IDs
*
* @param {Array} chatList
- * @return {Promise} only used internally when fetchAll() is called
+ * @return {Promise} only used internally when fetchAllReports() is called
*/
function fetchChatReportsByIDs(chatList) {
let fetchedReports;
@@ -284,7 +281,32 @@ function fetchChatReportsByIDs(chatList) {
.then(({reports}) => {
Log.info('[Report] successfully fetched report data', true);
fetchedReports = reports;
- return Promise.all(_.map(fetchedReports, updateIOUReportData));
+ return Promise.all(_.map(fetchedReports, (chatReport) => {
+ const reportActionList = chatReport.reportActionList || [];
+ const containsIOUAction = _.any(reportActionList,
+ reportAction => reportAction.action === CONST.REPORT.ACTIONS.TYPE.IOU);
+
+ // If there aren't any IOU actions, we don't need to fetch any additional data
+ if (!containsIOUAction) {
+ return;
+ }
+
+ // Group chat reports cannot and should not be associated with a specific IOU report
+ const participants = getParticipantEmailsFromReport(chatReport);
+ if (participants.length > 1) {
+ return;
+ }
+ if (participants.length === 0) {
+ Log.alert('[Report] Report with IOU action but does not have any participant.', true, {
+ reportID: chatReport.reportID,
+ participants,
+ });
+ return;
+ }
+
+ return fetchIOUReportID(participants[0])
+ .then(iouReportID => fetchIOUReport(iouReportID, chatReport.reportID));
+ }));
})
.then((iouReportObjects) => {
// Process the reports and store them in Onyx. At the same time we'll save the simplified reports in this
@@ -315,7 +337,6 @@ function fetchChatReportsByIDs(chatList) {
// than updating props for each report and re-rendering had merge been used.
Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_IOUS, reportIOUData);
Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, simplifiedReports);
- Onyx.set(ONYXKEYS.INITIAL_REPORT_DATA_LOADED, true);
// Fetch the personal details if there are any
PersonalDetails.getFromReportParticipants(Object.values(simplifiedReports));
@@ -324,6 +345,29 @@ function fetchChatReportsByIDs(chatList) {
});
}
+/**
+ * Given IOU object and chat report ID save the data to Onyx.
+ *
+ * @param {Object} iouReportObject
+ * @param {Number} iouReportObject.stateNum
+ * @param {Number} iouReportObject.total
+ * @param {Number} iouReportObject.reportID
+ * @param {Number} chatReportID
+ */
+function setLocalIOUReportData(iouReportObject, chatReportID) {
+ const chatReportObject = {
+ hasOutstandingIOU: iouReportObject.stateNum === 1 && iouReportObject.total !== 0,
+ iouReportID: iouReportObject.reportID,
+ };
+ if (!chatReportObject.hasOutstandingIOU) {
+ chatReportObject.iouReportID = null;
+ }
+ const iouReportKey = `${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReportObject.reportID}`;
+ const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`;
+ Onyx.merge(iouReportKey, iouReportObject);
+ Onyx.merge(reportKey, chatReportObject);
+}
+
/**
* Update the lastRead actionID and timestamp in local memory and Onyx
*
@@ -417,6 +461,20 @@ function updateReportWithNewAction(reportID, reportAction) {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, reportActionsToMerge);
+ // If chat report receives an action with IOU, update IOU object
+ if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) {
+ const chatReport = lodashGet(allReports, reportID);
+ const iouReportID = lodashGet(chatReport, 'iouReportID');
+ if (iouReportID) {
+ fetchIOUReport(iouReportID, reportID)
+ .then(iouReportObject => setLocalIOUReportData(iouReportObject, reportID));
+ } else if (!chatReport || chatReport.participants.length === 1) {
+ fetchIOUReportID(chatReport ? chatReport.participants[0] : reportAction.actorEmail)
+ .then(iouID => fetchIOUReport(iouID, reportID))
+ .then(iouReportObject => setLocalIOUReportData(iouReportObject, reportID));
+ }
+ }
+
if (!ActiveClientManager.isClientTheLeader()) {
console.debug('[LOCAL_NOTIFICATION] Skipping notification because this client is not the leader');
return;
@@ -577,56 +635,34 @@ function unsubscribeFromReportChannel(reportID) {
/**
* Get the report ID for a chat report for a specific
- * set of participants and redirect to it.
+ * set of participants and navigate to it if wanted.
*
* @param {String[]} participants
+ * @param {Boolean} shouldNavigate
+ * @returns {Promise}
*/
-function fetchOrCreateChatReport(participants) {
+function fetchOrCreateChatReport(participants, shouldNavigate = true) {
if (participants.length < 2) {
- throw new Error('fetchOrCreateChatReport() must have at least two participants');
+ throw new Error('fetchOrCreateChatReport() must have at least two participants.');
}
- API.CreateChatReport({
+ return API.CreateChatReport({
emailList: participants.join(','),
})
.then((data) => {
if (data.jsonCode !== 200) {
- throw new Error(data.message);
+ console.error(data.message);
+ return;
}
// Merge report into Onyx
- const reportID = data.reportID;
- Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {reportID});
-
- // Redirect the logged in person to the new report
- Navigation.navigate(ROUTES.getReportRoute(reportID));
- });
-}
-
-/**
- * Get all chat reports and provide the proper report name
- * by fetching sharedReportList and personalDetails
- *
- * @returns {Promise} only used internally when fetchAll() is called
- */
-function fetchChatReports() {
- return API.Get({
- returnValueList: 'chatList',
- })
- .then((response) => {
- if (response.jsonCode !== 200) {
- return;
- }
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${data.reportID}`, {reportID: data.reportID});
- // Get all the chat reports if they have any, otherwise create one with concierge
- if (lodashGet(response, 'chatList', []).length) {
- // The string cast here is necessary as Get rvl='chatList' may return an int
- fetchChatReportsByIDs(String(response.chatList).split(','));
- } else {
- fetchOrCreateChatReport([currentUserEmail, 'concierge@expensify.com']);
+ if (shouldNavigate) {
+ // Redirect the logged in person to the new report
+ Navigation.navigate(ROUTES.getReportRoute(data.reportID));
}
-
- return response.chatList;
+ return data.reportID;
});
}
@@ -674,30 +710,60 @@ function fetchActions(reportID, offset) {
*
* @param {Boolean} shouldRedirectToReport this is set to false when the network reconnect code runs
* @param {Boolean} shouldRecordHomePageTiming whether or not performance timing should be measured
+ * @param {Boolean} shouldDelayActionsFetch when the app loads we want to delay the fetching of additional actions
*/
-function fetchAll(shouldRedirectToReport = true, shouldRecordHomePageTiming = false) {
- fetchChatReports()
- .then((reportIDs) => {
- if (shouldRedirectToReport) {
- // Update currentlyViewedReportID to be our first reportID from our report collection if we don't have
- // one already.
- if (lastViewedReportID) {
- return;
- }
+function fetchAllReports(
+ shouldRedirectToReport = true,
+ shouldRecordHomePageTiming = false,
+ shouldDelayActionsFetch = false,
+) {
+ let reportIDs = [];
+
+ API.Get({
+ returnValueList: 'chatList',
+ })
+ .then((response) => {
+ if (response.jsonCode !== 200) {
+ return;
+ }
+
+ // The string cast here is necessary as Get rvl='chatList' may return an int
+ reportIDs = String(response.chatList).split(',');
- const firstReportID = _.first(reportIDs);
- const currentReportID = firstReportID ? String(firstReportID) : '';
- Onyx.merge(ONYXKEYS.CURRENTLY_VIEWED_REPORTID, currentReportID);
+ // Get all the chat reports if they have any, otherwise create one with concierge
+ if (reportIDs.length) {
+ return fetchChatReportsByIDs(reportIDs);
}
- Log.info('[Report] Fetching report actions for reports', true, {reportIDs});
- _.each(reportIDs, (reportID) => {
- fetchActions(reportID);
- });
+ return fetchOrCreateChatReport([currentUserEmail, 'concierge@expensify.com']);
+ })
+ .then(() => {
+ Onyx.set(ONYXKEYS.INITIAL_REPORT_DATA_LOADED, true);
if (shouldRecordHomePageTiming) {
Timing.end(CONST.TIMING.HOMEPAGE_REPORTS_LOADED);
}
+
+ // Optionally delay fetching report history as it significantly increases sign in to interactive time
+ _.delay(() => {
+ Log.info('[Report] Fetching report actions for reports', true, {reportIDs});
+ _.each(reportIDs, (reportID) => {
+ fetchActions(reportID);
+ });
+
+ // We are waiting 8 seconds since this provides a good time window to allow the UI to finish loading before
+ // bogging it down with more requests and operations.
+ }, shouldDelayActionsFetch ? 8000 : 0);
+
+ // Update currentlyViewedReportID to be our first reportID from our report collection if we don't have
+ // one already.
+ if (!shouldRedirectToReport || lastViewedReportID) {
+ return;
+ }
+
+ const firstReportID = _.first(reportIDs);
+ const currentReportID = firstReportID ? String(firstReportID) : '';
+ Onyx.merge(ONYXKEYS.CURRENTLY_VIEWED_REPORTID, currentReportID);
});
}
@@ -894,11 +960,11 @@ Onyx.connect({
// When the app reconnects from being offline, fetch all of the reports and their actions
NetworkConnection.onReconnect(() => {
- fetchAll(false);
+ fetchAllReports(false);
});
export {
- fetchAll,
+ fetchAllReports,
fetchActions,
fetchOrCreateChatReport,
addAction,
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index ecf3d7db91f..f711ce21a83 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -55,7 +55,7 @@ const propTypes = {
// participants associated with current report
participants: PropTypes.arrayOf(PropTypes.string),
- }).isRequired,
+ }),
/* Is the report view covered by the drawer */
isDrawerOpen: PropTypes.bool.isRequired,
@@ -77,6 +77,7 @@ const propTypes = {
const defaultProps = {
comment: '',
modal: {},
+ report: {},
network: {isOffline: false},
};
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 51a48e7dc3e..b6f9e4ec3f0 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -13,6 +13,8 @@ import PopoverWithMeasuredContent from '../../../components/PopoverWithMeasuredC
import ReportActionItemSingle from './ReportActionItemSingle';
import ReportActionItemGrouped from './ReportActionItemGrouped';
import ReportActionContextMenu from './ReportActionContextMenu';
+import ReportActionItemIOUPreview from '../../../components/ReportActionItemIOUPreview';
+import ReportActionItemMessage from './ReportActionItemMessage';
import UnreadActionIndicator from '../../../components/UnreadActionIndicator';
const propTypes = {
@@ -25,10 +27,24 @@ const propTypes = {
// Should the comment have the appearance of being grouped with the previous comment?
displayAsGroup: PropTypes.bool.isRequired,
+ // Is this the most recent IOU Action?
+ isMostRecentIOUReportAction: PropTypes.bool.isRequired,
+
+ // Whether there is an outstanding amount in IOU
+ hasOutstandingIOU: PropTypes.bool,
+
+ // IOU report ID associated with current report
+ iouReportID: PropTypes.number,
+
// Should we display the new indicator on top of the comment?
shouldDisplayNewIndicator: PropTypes.bool.isRequired,
};
+const defaultProps = {
+ iouReportID: undefined,
+ hasOutstandingIOU: false,
+};
+
class ReportActionItem extends Component {
constructor(props) {
super(props);
@@ -50,6 +66,9 @@ class ReportActionItem extends Component {
shouldComponentUpdate(nextProps, nextState) {
return this.state.isPopoverVisible !== nextState.isPopoverVisible
|| this.props.displayAsGroup !== nextProps.displayAsGroup
+ || this.props.isMostRecentIOUReportAction !== nextProps.isMostRecentIOUReportAction
+ || this.props.hasOutstandingIOU !== nextProps.hasOutstandingIOU
+ || this.props.iouReportID !== nextProps.iouReportID
|| (this.props.shouldDisplayNewIndicator !== nextProps.shouldDisplayNewIndicator)
|| !_.isEqual(this.props.action, nextProps.action);
}
@@ -85,6 +104,16 @@ class ReportActionItem extends Component {
}
render() {
+ const children = this.props.action.actionName === 'IOU'
+ ? (
+
+ )
+ : ;
return (
@@ -95,8 +124,16 @@ class ReportActionItem extends Component {
)}
{!this.props.displayAsGroup
- ?
- : }
+ ? (
+
+ {children}
+
+ )
+ : (
+
+ {children}
+
+ )}
(
+const ReportActionItemGrouped = ({children}) => (
-
+ {children}
);
diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js
index 9b026c7db55..fe28b1a3868 100644
--- a/src/pages/home/report/ReportActionItemMessage.js
+++ b/src/pages/home/report/ReportActionItemMessage.js
@@ -2,6 +2,8 @@ import React from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
+import {withOnyx} from 'react-native-onyx';
+import ONYXKEYS from '../../../ONYXKEYS';
import styles from '../../../styles/styles';
import ReportActionItemFragment from './ReportActionItemFragment';
import ReportActionPropTypes from './ReportActionPropTypes';
@@ -9,11 +11,20 @@ import ReportActionPropTypes from './ReportActionPropTypes';
const propTypes = {
// The report action
action: PropTypes.shape(ReportActionPropTypes).isRequired,
+
+ // Information about the network
+ network: PropTypes.shape({
+ // Is the network currently offline or not
+ isOffline: PropTypes.bool,
+ }),
+};
+
+const defaultProps = {
+ network: {isOffline: false},
};
-const ReportActionItemMessage = ({action}) => {
- // reportActionID is only present when the action is saved onto server.
- const isUnsent = action.loading && !action.reportActionID;
+const ReportActionItemMessage = ({action, network}) => {
+ const isUnsent = network.isOffline && action.loading;
return (
{_.map(_.compact(action.message), (fragment, index) => (
@@ -29,6 +40,11 @@ const ReportActionItemMessage = ({action}) => {
};
ReportActionItemMessage.propTypes = propTypes;
+ReportActionItemMessage.defaultProps = defaultProps;
ReportActionItemMessage.displayName = 'ReportActionItemMessage';
-export default ReportActionItemMessage;
+export default withOnyx({
+ network: {
+ key: ONYXKEYS.NETWORK,
+ },
+})(ReportActionItemMessage);
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index e9cc33f9d08..84e08efdc84 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -4,7 +4,6 @@ import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import _ from 'underscore';
import ReportActionPropTypes from './ReportActionPropTypes';
-import ReportActionItemMessage from './ReportActionItemMessage';
import ReportActionItemFragment from './ReportActionItemFragment';
import styles from '../../../styles/styles';
import CONST from '../../../CONST';
@@ -18,10 +17,21 @@ const propTypes = {
action: PropTypes.shape(ReportActionPropTypes).isRequired,
// All of the personalDetails
- personalDetails: PropTypes.objectOf(personalDetailsPropType).isRequired,
+ personalDetails: PropTypes.objectOf(personalDetailsPropType),
+
+ // Children view component for this action item
+ children: PropTypes.node.isRequired,
+};
+
+const defaultProps = {
+ personalDetails: {},
};
-const ReportActionItemSingle = ({action, personalDetails}) => {
+const ReportActionItemSingle = ({
+ action,
+ personalDetails,
+ children,
+}) => {
const {avatar, displayName} = personalDetails[action.actorEmail] || {};
const avatarUrl = action.automatic
? `${CONST.CLOUDFRONT_URL}/images/icons/concierge_2019.svg`
@@ -52,13 +62,14 @@ const ReportActionItemSingle = ({action, personalDetails}) => {
))}
-
+ {children}
);
};
ReportActionItemSingle.propTypes = propTypes;
+ReportActionItemSingle.defaultProps = defaultProps;
export default withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS,
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index 347dd3d5ee4..398fd16bb73 100644
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -40,6 +40,12 @@ const propTypes = {
// The largest sequenceNumber on this report
maxSequenceNumber: PropTypes.number,
+
+ // Whether there is an outstanding amount in IOU
+ hasOutstandingIOU: PropTypes.bool,
+
+ // IOU report ID associated with current report
+ iouReportID: PropTypes.number,
}),
// Array of report actions for this report
@@ -56,6 +62,7 @@ const defaultProps = {
report: {
unreadActionCount: 0,
maxSequenceNumber: 0,
+ hasOutstandingIOU: false,
},
reportActions: {},
session: {},
@@ -83,6 +90,7 @@ class ReportActionsView extends React.Component {
};
this.updateSortedReportActions(props.reportActions);
+ this.updateMostRecentIOUReportActionNumber(props.reportActions);
}
componentDidMount() {
@@ -97,6 +105,7 @@ class ReportActionsView extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (!_.isEqual(nextProps.reportActions, this.props.reportActions)) {
this.updateSortedReportActions(nextProps.reportActions);
+ this.updateMostRecentIOUReportActionNumber(nextProps.reportActions);
return true;
}
@@ -104,6 +113,11 @@ class ReportActionsView extends React.Component {
return true;
}
+ if (this.props.report.hasOutstandingIOU !== nextProps.report.hasOutstandingIOU
+ || this.props.report.iouReportID !== nextProps.report.iouReportID) {
+ return true;
+ }
+
return false;
}
@@ -232,6 +246,19 @@ class ReportActionsView extends React.Component {
updateLastReadActionID(this.props.reportID, maxVisibleSequenceNumber);
}
+ /**
+ * Finds and updates most recent IOU report action number
+ *
+ * @param {Array<{sequenceNumber, actionName}>} reportActions
+ */
+ updateMostRecentIOUReportActionNumber(reportActions) {
+ this.mostRecentIOUReportSequenceNumber = _.chain(reportActions)
+ .sortBy('sequenceNumber')
+ .filter(action => action.actionName === 'IOU')
+ .max(action => action.sequenceNumber)
+ .value().sequenceNumber;
+ }
+
/**
* This function is triggered from the ref callback for the scrollview. That way it can be scrolled once all the
* items have been rendered. If the number of actions has changed since it was last rendered, then
@@ -286,6 +313,9 @@ class ReportActionsView extends React.Component {
displayAsGroup={this.isConsecutiveActionMadeByPreviousActor(index)}
shouldDisplayNewIndicator={this.initialNewMarkerPosition > 0
&& item.action.sequenceNumber === this.initialNewMarkerPosition}
+ isMostRecentIOUReportAction={item.action.sequenceNumber === this.mostRecentIOUReportSequenceNumber}
+ iouReportID={this.props.report.iouReportID}
+ hasOutstandingIOU={this.props.report.hasOutstandingIOU}
/>
);
}
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 7f24fa96180..c42f2e4a4e0 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -68,6 +68,9 @@ const propTypes = {
// The chat priority mode
priorityMode: PropTypes.string,
+
+ // Whether we have the necessary report data to load the sidebar
+ initialReportDataLoaded: PropTypes.bool,
};
const defaultProps = {
@@ -80,6 +83,7 @@ const defaultProps = {
network: null,
currentlyViewedReportID: '',
priorityMode: CONST.PRIORITY_MODE.DEFAULT,
+ initialReportDataLoaded: false,
};
class SidebarLinks extends React.Component {
@@ -88,6 +92,11 @@ class SidebarLinks extends React.Component {
}
render() {
+ // Wait until the reports are actually loaded before displaying the LHN
+ if (!this.props.initialReportDataLoaded) {
+ return null;
+ }
+
const activeReportID = parseInt(this.props.currentlyViewedReportID, 10);
const {recentReports} = getSidebarOptions(
@@ -184,5 +193,8 @@ export default compose(
priorityMode: {
key: ONYXKEYS.NVP_PRIORITY_MODE,
},
+ initialReportDataLoaded: {
+ key: ONYXKEYS.INITIAL_REPORT_DATA_LOADED,
+ },
}),
)(SidebarLinks);
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 3f3fa97e678..baf5dc75041 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -122,6 +122,7 @@ const styles = {
},
buttonSmall: {
+ borderRadius: variables.componentBorderRadiusNormal,
height: variables.componentSizeSmall,
paddingTop: 6,
paddingRight: 10,
@@ -132,6 +133,9 @@ const styles = {
buttonSmallText: {
fontSize: variables.fontSizeSmall,
lineHeight: 16,
+ fontFamily: fontFamily.GTA_BOLD,
+ fontWeight: fontWeightBold,
+ textAlign: 'center',
},
buttonSuccess: {
@@ -943,6 +947,14 @@ const styles = {
borderColor: 'transparent',
},
+ secondAvatarInline: {
+ bottom: -3,
+ right: -25,
+ borderWidth: 3,
+ borderRadius: 18,
+ borderColor: themeColors.componentBG,
+ },
+
avatarNormal: {
height: variables.componentSizeNormal,
width: variables.componentSizeNormal,
@@ -1308,6 +1320,20 @@ const styles = {
color: themeColors.heading,
}, 0),
+ iouPreviewBox: {
+ borderColor: themeColors.border,
+ borderWidth: 1,
+ borderRadius: variables.componentBorderRadiusCard,
+ padding: 20,
+ marginTop: 16,
+ maxWidth: 300,
+ width: '100%',
+ },
+
+ iouPreviewBoxAvatar: {
+ marginRight: -10,
+ },
+
noScrollbars: {
scrollbarWidth: 'none',
},
diff --git a/src/styles/variables.js b/src/styles/variables.js
index 9b70c5d5b07..fff0f2fd099 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -6,6 +6,7 @@ export default {
componentBorderRadius: 8,
componentBorderRadiusSmall: 4,
componentBorderRadiusNormal: 8,
+ componentBorderRadiusCard: 12,
avatarSizeNormal: 40,
avatarSizeSmall: 28,
fontSizeSmall: 11,