diff --git a/src/IONKEYS.js b/src/IONKEYS.js index 6f8204e68152..366339719a57 100644 --- a/src/IONKEYS.js +++ b/src/IONKEYS.js @@ -2,16 +2,15 @@ * This is a file containing constants for all the top level keys in our store */ export default { - ACTIVE_CLIENT_IDS: 'activeClientIDs', - APP_REDIRECT_TO: 'app_redirectTo', - CURRENT_URL: 'current_url', + ACTIVE_CLIENTS: 'activeClients', + APP_REDIRECT_TO: 'appRedirectTo', + CURRENT_URL: 'currentURL', CREDENTIALS: 'credentials', - MY_PERSONAL_DETAILS: 'my_personal_details', + FIRST_REPORT_ID: 'firstReportID', + MY_PERSONAL_DETAILS: 'myPersonalDetails', NETWORK: 'network', - PERSONAL_DETAILS: 'personal_details', + PERSONAL_DETAILS: 'personalDetails', REPORT: 'report', - REPORT_ACTION: 'reportAction', - REPORT_HISTORY: 'report_history', - FIRST_REPORT_ID: 'first_report_id', + REPORT_ACTIONS: 'reportActions', SESSION: 'session', }; diff --git a/src/lib/ActiveClientManager.js b/src/lib/ActiveClientManager.js index 2ffde8ce8be6..8d68c313237e 100644 --- a/src/lib/ActiveClientManager.js +++ b/src/lib/ActiveClientManager.js @@ -10,7 +10,7 @@ const clientID = Guid(); * * @returns {Promise} */ -const init = () => Ion.merge(IONKEYS.ACTIVE_CLIENT_IDS, {clientID}); +const init = () => Ion.merge(IONKEYS.ACTIVE_CLIENTS, {clientID}); /** * Remove this client ID from the array of active client IDs when this client is exited @@ -18,9 +18,9 @@ const init = () => Ion.merge(IONKEYS.ACTIVE_CLIENT_IDS, {clientID}); * @returns {Promise} */ function removeClient() { - return Ion.get(IONKEYS.ACTIVE_CLIENT_IDS) + return Ion.get(IONKEYS.ACTIVE_CLIENTS) .then(activeClientIDs => _.omit(activeClientIDs, clientID)) - .then(newActiveClientIDs => Ion.set(IONKEYS.ACTIVE_CLIENT_IDS, newActiveClientIDs)); + .then(newActiveClientIDs => Ion.set(IONKEYS.ACTIVE_CLIENTS, newActiveClientIDs)); } /** @@ -29,7 +29,7 @@ function removeClient() { * @returns {Promise} */ function isClientTheLeader() { - return Ion.get(IONKEYS.ACTIVE_CLIENT_IDS) + return Ion.get(IONKEYS.ACTIVE_CLIENTS) .then(activeClientIDs => _.first(activeClientIDs) === clientID); } diff --git a/src/lib/Network.js b/src/lib/Network.js index 9c05d296c0a0..dbad2332c86e 100644 --- a/src/lib/Network.js +++ b/src/lib/Network.js @@ -16,10 +16,10 @@ let isOffline; Ion.connect({key: IONKEYS.NETWORK, path: 'isOffline', callback: val => isOffline = val}); let credentials; -Ion.connect({key: IONKEYS.CREDENTIALS, callback: c => credentials = c}); +Ion.connect({key: IONKEYS.CREDENTIALS, callback: val => credentials = val}); let currentUrl; -Ion.connect({key: IONKEYS.CURRENT_URL, callback: url => currentUrl = url}); +Ion.connect({key: IONKEYS.CURRENT_URL, callback: val => currentUrl = val}); /** * When authTokens expire they will automatically be refreshed. diff --git a/src/lib/actions/Report.js b/src/lib/actions/Report.js index 928e9521e7fd..fcdede6a9167 100644 --- a/src/lib/actions/Report.js +++ b/src/lib/actions/Report.js @@ -11,34 +11,72 @@ import ExpensiMark from '../ExpensiMark'; import Notification from '../Notification'; import * as PersonalDetails from './PersonalDetails'; +let currentUserEmail; +let currentUserAccountID; +Ion.connect({ + key: IONKEYS.SESSION, + callback: (val) => { + // When signed out, val is undefined + if (val) { + currentUserEmail = val.email; + currentUserAccountID = val.accountID; + } + } +}); + +let currentURL; +Ion.connect({ + key: IONKEYS.CURRENT_URL, + callback: val => currentURL = val, +}); + +// Use a regex pattern here for an exact match so it doesn't also match "my_personal_details" +let personalDetails; +Ion.connect({ + key: `^${IONKEYS.PERSONAL_DETAILS}$`, + callback: val => personalDetails = val, +}); + +let myPersonalDetails; +Ion.connect({ + key: IONKEYS.MY_PERSONAL_DETAILS, + callback: val => myPersonalDetails = val, +}); + +const reportMaxSequenceNumbers = {}; + // List of reportIDs that we define in .env const configReportIDs = CONFIG.REPORT_IDS.split(',').map(Number); /** - * Checks the report to see if there are any unread history items + * Checks the report to see if there are any unread action items * - * @param {string} accountID * @param {object} report * @returns {boolean} */ -function hasUnreadHistoryItems(accountID, report) { - const usersLastReadActionID = lodashGet(report, ['reportNameValuePairs', `lastReadActionID_${accountID}`]); - if (!usersLastReadActionID || report.reportActionList.length === 0) { +function hasUnreadActions(report) { + const usersLastReadActionID = lodashGet(report, [ + 'reportNameValuePairs', + `lastReadActionID_${currentUserAccountID}`, + ]); + + if (report.reportActionList.length === 0) { return false; } - // Find the most recent sequence number from the report history - const sequenceNumber = _.chain(report.reportActionList) - .pluck('sequenceNumber') - .max() - .value(); + if (!usersLastReadActionID) { + return true; + } + + // Find the most recent sequence number from the report actions + const maxSequenceNumber = reportMaxSequenceNumbers[report.reportID]; - if (!sequenceNumber) { + if (!maxSequenceNumber) { return false; } // There are unread items if the last one the user has read is less than the highest sequence number we have - return usersLastReadActionID < sequenceNumber; + return usersLastReadActionID < maxSequenceNumber; } /** @@ -49,15 +87,14 @@ function hasUnreadHistoryItems(accountID, report) { * @param {number} report.reportID * @param {string} report.reportName * @param {object} report.reportNameValuePairs - * @param {string} accountID * @returns {object} */ -function getSimplifiedReportObject(report, accountID) { +function getSimplifiedReportObject(report) { return { reportID: report.reportID, reportName: report.reportName, reportNameValuePairs: report.reportNameValuePairs, - hasUnread: hasUnreadHistoryItems(accountID, report), + hasUnread: hasUnreadActions(report), pinnedReport: configReportIDs.includes(report.reportID), }; } @@ -66,11 +103,9 @@ function getSimplifiedReportObject(report, accountID) { * Returns a generated report title based on the participants * * @param {array} sharedReportList - * @param {object} personalDetails - * @param {string} currentUserEmail * @return {string} */ -function getChatReportName(sharedReportList, personalDetails, currentUserEmail) { +function getChatReportName(sharedReportList) { return _.chain(sharedReportList) .map(participant => participant.email) .filter(participant => participant !== currentUserEmail) @@ -87,19 +122,12 @@ function getChatReportName(sharedReportList, personalDetails, currentUserEmail) * @return {Promise} */ function fetchChatReportsByIDs(chatList) { - let currentUserEmail; - let currentUserAccountID; let fetchedReports; - return Ion.get(IONKEYS.SESSION) - .then((session) => { - currentUserEmail = session.email; - currentUserAccountID = session.accountID; - return queueRequest('Get', { - returnValueList: 'reportStuff', - reportIDList: chatList.join(','), - shouldLoadOptionalKeys: true, - }); - }) + return queueRequest('Get', { + returnValueList: 'reportStuff', + reportIDList: chatList.join(','), + shouldLoadOptionalKeys: true, + }) .then(({reports}) => { fetchedReports = reports; @@ -115,20 +143,18 @@ function fetchChatReportsByIDs(chatList) { .unique() .value(); - return PersonalDetails.getForEmails(emails.join(',')); - }) - .then((personalDetails) => { + // Fetch the person details if there are any + if (emails && emails.length !== 0) { + PersonalDetails.getForEmails(emails.join(',')); + } + + // Process the reports and store them in Ion const ionPromises = _.map(fetchedReports, (report) => { - // Store only the absolute bare minimum of data in Ion because space is limited - const newReport = getSimplifiedReportObject(report, currentUserAccountID); + const newReport = getSimplifiedReportObject(report); if (lodashGet(report, 'reportNameValuePairs.type') === 'chat') { - newReport.reportName = getChatReportName( - report.sharedReportList, - personalDetails, - currentUserEmail - ); + newReport.reportName = getChatReportName(report.sharedReportList); } // Merge the data into Ion. Don't use set() here or multiSet() because then that would @@ -140,123 +166,73 @@ function fetchChatReportsByIDs(chatList) { }); } -/** - * Get the history of a report - * - * @param {string} reportID - * @returns {Promise} - */ -function fetchHistory(reportID) { - return queueRequest('Report_GetHistory', { - reportID, - offset: 0, - }) - .then((data) => { - const indexedData = _.indexBy(data.history, 'sequenceNumber'); - Ion.set(`${IONKEYS.REPORT_HISTORY}_${reportID}`, indexedData); - }); -} - /** * Updates a report in the store with a new report action * - * @param {string} reportID + * @param {number} reportID * @param {object} reportAction */ function updateReportWithNewAction(reportID, reportAction) { - let currentUserEmail; - let ionReportFound = true; - - Ion.get(`${IONKEYS.REPORT}_${reportID}`, 'reportID') - .then((ionReportID) => { - // This is necessary for local development because there will be pusher events from other engineers with - // different reportIDs. This means that while in development it's not possible to make new chats appear - // by creating chats then leaving comments in other windows. - if (!CONFIG.IS_IN_PRODUCTION && !ionReportID) { - throw new Error('report does not exist in the store, so ignoring new comments'); - } - - // When handling a realtime update for a chat that does not yet exist in our store we - // need to fetch it so that we can properly navigate to it. This enables us populate - // newly created chats in the LHN without requiring a full refresh of the app. - if (!ionReportID) { - ionReportFound = false; - return fetchChatReportsByIDs([reportID]) - .then(() => fetchHistory(reportID)) - .then(() => Ion.get(`${IONKEYS.REPORT_HISTORY}_${reportID}`)); - } - - // Get the report history and return that to the next chain - return Ion.get(`${IONKEYS.REPORT_HISTORY}_${reportID}`); - }) + // Always merge the reportID into Ion + // If the report doesn't exist in Ion yet, then all the rest of the data will be filled out + // by handleReportChanged + Ion.merge(`${IONKEYS.REPORT}_${reportID}`, { + reportID, + maxSequenceNumber: reportAction.sequenceNumber, + }); - // Look to see if the report action from pusher already exists or not (it would exist if it's a comment just - // written by the user). If the action doesn't exist, then update the unread flag on the report so the user - // knows there is a new comment - .then((reportHistory) => { - // If there was no ionReport found then we cannot check sequence number because the fetchHistory in the - // previous block will give us the most up to date information. - if (!ionReportFound || (reportHistory && !reportHistory[reportAction.sequenceNumber])) { - Ion.merge(`${IONKEYS.REPORT}_${reportID}`, {hasUnread: true}); - } - return reportHistory || {}; - }) + const previousMaxSequenceNumber = reportMaxSequenceNumbers[reportID]; + const newMaxSequenceNumber = reportAction.sequenceNumber; - // Put the report action from pusher into the history, it's OK to overwrite it if it already exists - .then(reportHistory => ({ - ...reportHistory, - [reportAction.sequenceNumber]: reportAction, - })) + // Mark the report as unread if there is a new max sequence number + if (newMaxSequenceNumber > previousMaxSequenceNumber) { + Ion.merge(`${IONKEYS.REPORT}_${reportID}`, { + hasUnread: true, + maxSequenceNumber: newMaxSequenceNumber, + }); + } - // Put the report history back into Ion - .then(reportHistory => Ion.set(`${IONKEYS.REPORT_HISTORY}_${reportID}`, reportHistory)) + // Add the action into Ion + Ion.merge(`${IONKEYS.REPORT_ACTIONS}_${reportID}`, { + [reportAction.sequenceNumber]: reportAction, + }); - // Check to see if we need to show a notification for this report - .then(() => Ion.get(IONKEYS.SESSION, 'email')) - .then((email) => { - currentUserEmail = email; - return Ion.get(IONKEYS.CURRENT_URL); - }) - .then((currentUrl) => { - // If this comment is from the current user we don't want to parrot whatever they wrote back to them. - if (reportAction.actorEmail === currentUserEmail) { - return; - } + // If this comment is from the current user we don't want to parrot whatever they wrote back to them. + if (reportAction.actorEmail === currentUserEmail) { + console.debug('[NOTIFICATION] No notification because comment is from the currently logged in user'); + return; + } - const currentReportID = Number(lodashGet(currentUrl.split('/'), [1], 0)); + const currentReportID = Number(lodashGet(currentURL.split('/'), [1], 0)); - // If we are currently viewing this report do not show a notification. - if (reportID === currentReportID) { - return; - } + // If we are currently viewing this report do not show a notification. + if (reportID === currentReportID) { + console.debug('[NOTIFICATION] No notification because it was a comment for the current report'); + return; + } - Notification.showCommentNotification({ - reportAction, - onClick: () => { - // Navigate to this report onClick - Ion.set(IONKEYS.APP_REDIRECT_TO, `/${reportID}`); - } - }); - }); + console.debug('[NOTIFICATION] Creating notification'); + Notification.showCommentNotification({ + reportAction, + onClick: () => { + // Navigate to this report onClick + Ion.set(IONKEYS.APP_REDIRECT_TO, `/${reportID}`); + } + }); } /** * Initialize our pusher subscriptions to listen for new report comments - * - * @returns {Promise} */ function subscribeToReportCommentEvents() { - return Ion.get(IONKEYS.SESSION, 'accountID') - .then((accountID) => { - const pusherChannelName = `private-user-accountID-${accountID}`; - if (Pusher.isSubscribed(pusherChannelName) || Pusher.isAlreadySubscribing(pusherChannelName)) { - return; - } + const pusherChannelName = `private-user-accountID-${currentUserAccountID}`; + if (Pusher.isSubscribed(pusherChannelName) || Pusher.isAlreadySubscribing(pusherChannelName)) { + return; + } - Pusher.subscribe(pusherChannelName, 'reportComment', (pushJSON) => { - updateReportWithNewAction(pushJSON.reportID, pushJSON.reportAction); - }); - }); + Pusher.subscribe(pusherChannelName, 'reportComment', (pushJSON) => { + updateReportWithNewAction(pushJSON.reportID, pushJSON.reportAction); + }); } /** @@ -294,147 +270,147 @@ function fetchAll() { reportFetchPromises.push(fetchChatReports()); return promiseAllSettled(reportFetchPromises) - .then(data => fetchedReports = _.compact(_.map(data, (promiseResult) => { - // Grab the report from the promise result which stores it in the `value` key - const report = lodashGet(promiseResult, 'value.reports', {}); - - // If there is no report found from the promise, return null - // Otherwise, grab the actual report object from the first index in the values array - return _.isEmpty(report) ? null : _.values(report)[0]; - }))) - .then(() => Ion.set(IONKEYS.FIRST_REPORT_ID, _.first(_.pluck(fetchedReports, 'reportID')) || 0)) - .then(() => Ion.get(IONKEYS.SESSION, 'accountID')) - .then((accountID) => { - const ionPromises = _.map(fetchedReports, (report) => { - // Store only the absolute bare minimum of data in Ion because space is limited - const newReport = getSimplifiedReportObject(report, accountID); + .then((data) => { + fetchedReports = _.compact(_.map(data, (promiseResult) => { + // Grab the report from the promise result which stores it in the `value` key + const report = lodashGet(promiseResult, 'value.reports', {}); + + // If there is no report found from the promise, return null + // Otherwise, grab the actual report object from the first index in the values array + return _.isEmpty(report) ? null : _.values(report)[0]; + })); + // Store the first report ID in Ion + Ion.set(IONKEYS.FIRST_REPORT_ID, _.first(_.pluck(fetchedReports, 'reportID')) || 0); + + _.each(fetchedReports, (report) => { // Merge the data into Ion. Don't use set() here or multiSet() because then that would // overwrite any existing data (like if they have unread messages) - return Ion.merge(`${IONKEYS.REPORT}_${report.reportID}`, newReport); + Ion.merge(`${IONKEYS.REPORT}_${report.reportID}`, getSimplifiedReportObject(report)); }); - return promiseAllSettled(ionPromises); - }) - .then(() => fetchedReports); + return fetchedReports; + }); +} + +/** + * Get the actions of a report + * + * @param {number} reportID + * @returns {Promise} + */ +function fetchActions(reportID) { + return queueRequest('Report_GetHistory', {reportID}) + .then((data) => { + const indexedData = _.indexBy(data.history, 'sequenceNumber'); + const maxSequenceNumber = _.chain(data.history) + .pluck('sequenceNumber') + .max() + .value(); + Ion.set(`${IONKEYS.REPORT_ACTIONS}_${reportID}`, indexedData); + Ion.merge(`${IONKEYS.REPORT}_${reportID}`, {maxSequenceNumber}); + }); } /** - * Get the chat report ID, and then the history, for a chat report for a specific + * Get the report ID, and then the actions, for a chat report for a specific * set of participants * * @param {string[]} participants - * @returns {Promise} + * @returns {Promise} resolves with reportID */ -function fetchChatReport(participants) { - let currentUserEmail; - let currentUserAccountID; - let personalDetails; +function fetchOrCreateChatReport(participants) { let reportID; - // Get the current users accountID and set it aside in a local variable - // which is used for checking if there are unread comments - return Ion.multiGet([IONKEYS.SESSION, IONKEYS.PERSONAL_DETAILS]) - .then((data) => { - currentUserEmail = data.session.email; - currentUserAccountID = data.session.accountID; - personalDetails = data.personal_details; - }) - - // Make a request to get the reportID for this list of participants - .then(() => queueRequest('CreateChatReport', { - emailList: participants.join(','), - })) + return queueRequest('CreateChatReport', { + emailList: participants.join(','), + }) - // Set aside the reportID in a local variable so it can be accessed in the rest of the chain - .then(data => reportID = data.reportID) + .then((data) => { + // Set aside the reportID in a local variable so it can be accessed in the rest of the chain + reportID = data.reportID; - // Make a request to get all the information about the report - .then(() => queueRequest('Get', { - returnValueList: 'reportStuff', - reportIDList: reportID, - shouldLoadOptionalKeys: true, - })) + // Make a request to get all the information about the report + return queueRequest('Get', { + returnValueList: 'reportStuff', + reportIDList: reportID, + shouldLoadOptionalKeys: true, + }); + }) // Put the report object into Ion .then((data) => { const report = data.reports[reportID]; // Store only the absolute bare minimum of data in Ion because space is limited - const newReport = getSimplifiedReportObject(report, currentUserAccountID); - newReport.reportName = getChatReportName(report.sharedReportList, personalDetails, currentUserEmail); + const newReport = getSimplifiedReportObject(report); + newReport.reportName = getChatReportName(report.sharedReportList); // Merge the data into Ion. Don't use set() here or multiSet() because then that would // overwrite any existing data (like if they have unread messages) - return Ion.merge(`${IONKEYS.REPORT}_${reportID}`, newReport); - }) + Ion.merge(`${IONKEYS.REPORT}_${reportID}`, newReport); - // Return the reportID as the final return value - .then(() => reportID); + // Return the reportID as the final return value + return reportID; + }); } /** - * Add a history item to a report + * Add an action item to a report * - * @param {string} reportID - * @param {string} reportComment - * @returns {Promise} + * @param {number} reportID + * @param {string} text */ -function addHistoryItem(reportID, reportComment) { - const historyKey = `${IONKEYS.REPORT_HISTORY}_${reportID}`; +function addAction(reportID, text) { + const actionKey = `${IONKEYS.REPORT_ACTIONS}_${reportID}`; // Convert the comment from MD into HTML because that's how it is stored in the database const parser = new ExpensiMark(); - const htmlComment = parser.replace(reportComment); - - return Ion.multiGet([historyKey, IONKEYS.SESSION, IONKEYS.PERSONAL_DETAILS]) - .then((values) => { - const reportHistory = values[historyKey]; - const email = values[IONKEYS.SESSION].email || ''; - const personalDetails = lodashGet(values, [IONKEYS.PERSONAL_DETAILS, email], {}); - - // The new sequence number will be one higher than the highest - let highestSequenceNumber = _.chain(reportHistory) - .pluck('sequenceNumber') - .max() - .value() || 0; - const newSequenceNumber = highestSequenceNumber + 1; - - // Optimistically add the new comment to the store before waiting to save it to the server - return Ion.set(historyKey, { - ...reportHistory, - [newSequenceNumber]: { - actionName: 'ADDCOMMENT', - actorEmail: email, - person: [ - { - style: 'strong', - text: personalDetails.displayName || email, - type: 'TEXT' - } - ], - automatic: false, - sequenceNumber: ++highestSequenceNumber, - avatar: personalDetails.avatarURL, - timestamp: moment().unix(), - message: [ - { - type: 'COMMENT', - html: htmlComment, - - // Remove HTML from text when applying optimistic offline comment - text: htmlComment.replace(/<[^>]*>?/gm, ''), - } - ], - isFirstItem: false, - isAttachmentPlaceHolder: false, + const htmlComment = parser.replace(text); + + // The new sequence number will be one higher than the highest + let highestSequenceNumber = reportMaxSequenceNumbers[reportID] || 0; + const newSequenceNumber = highestSequenceNumber + 1; + + // Update the report in Ion to have the new sequence number + Ion.merge(`${IONKEYS.REPORT}_${reportID}`, { + maxSequenceNumber: newSequenceNumber, + }); + + // Optimistically add the new comment to the store before waiting to save it to the server + Ion.merge(actionKey, { + [newSequenceNumber]: { + actionName: 'ADDCOMMENT', + actorEmail: currentUserEmail, + person: [ + { + style: 'strong', + text: myPersonalDetails.displayName || currentUserEmail, + type: 'TEXT' } - }); - }) - .then(() => queueRequest('Report_AddComment', { - reportID, - reportComment: htmlComment, - })); + ], + automatic: false, + sequenceNumber: ++highestSequenceNumber, + avatar: myPersonalDetails.avatarURL, + timestamp: moment().unix(), + message: [ + { + type: 'COMMENT', + html: htmlComment, + + // Remove HTML from text when applying optimistic offline comment + text: htmlComment.replace(/<[^>]*>?/gm, ''), + } + ], + isFirstItem: false, + isAttachmentPlaceHolder: false, + } + }); + + queueRequest('Report_AddComment', { + reportID, + reportComment: htmlComment, + }); } /** @@ -442,37 +418,54 @@ function addHistoryItem(reportID, reportComment) { * network layer handle the delayed write. * * @param {string} accountID - * @param {string} reportID + * @param {number} reportID * @param {number} sequenceNumber - * @returns {Promise} */ function updateLastReadActionID(accountID, reportID, sequenceNumber) { // Mark the report as not having any unread items - return Ion.merge(`${IONKEYS.REPORT}_${reportID}`, { + Ion.merge(`${IONKEYS.REPORT}_${reportID}`, { hasUnread: false, reportNameValuePairs: { [`lastReadActionID_${accountID}`]: sequenceNumber, } - }) + }); + - // Update the lastReadActionID on the report optimistically - .then(() => queueRequest('Report_SetLastReadActionID', { - accountID, - reportID, - sequenceNumber, - })); + // Update the lastReadActionID on the report optimistically + queueRequest('Report_SetLastReadActionID', { + accountID, + reportID, + sequenceNumber, + }); } -// When the app reconnects from being offline, fetch all of the reports and their history -onReconnect(() => { - fetchAll().then(reports => _.each(reports, report => fetchHistory(report.reportID))); +/** + * When a report changes in Ion, this fetches the report from the API if the report doesn't have a name + * and it keeps track of the max sequence number on the report actions. + * + * @param {object} report + */ +function handleReportChanged(report) { + if (report.reportName === undefined) { + fetchChatReportsByIDs([report.reportID]); + } + + reportMaxSequenceNumbers[report.reportID] = report.maxSequenceNumber; +} +Ion.connect({ + key: `${IONKEYS.REPORT}_[0-9]+$`, + callback: handleReportChanged }); +// When the app reconnects from being offline, fetch all of the reports and their actions +onReconnect(() => { + fetchAll().then(reports => _.each(reports, report => fetchActions(report.reportID))); +}); export { fetchAll, - fetchHistory, - fetchChatReport, - addHistoryItem, + fetchActions, + fetchOrCreateChatReport, + addAction, updateLastReadActionID, subscribeToReportCommentEvents, }; diff --git a/src/page/home/report/ReportHistoryCompose.js b/src/page/home/report/ReportActionCompose.js similarity index 91% rename from src/page/home/report/ReportHistoryCompose.js rename to src/page/home/report/ReportActionCompose.js index ca6a97674c8f..8eefa7333b5b 100644 --- a/src/page/home/report/ReportHistoryCompose.js +++ b/src/page/home/report/ReportActionCompose.js @@ -8,12 +8,9 @@ import sendIcon from '../../../../assets/images/icon-send.png'; const propTypes = { // A method to call when the form is submitted onSubmit: PropTypes.func.isRequired, - - // The ID of the report actions will be created for - reportID: PropTypes.number.isRequired, }; -class ReportHistoryCompose extends React.Component { +class ReportActionCompose extends React.Component { constructor(props) { super(props); @@ -69,7 +66,7 @@ class ReportHistoryCompose extends React.Component { return; } - this.props.onSubmit(this.props.reportID, trimmedComment); + this.props.onSubmit(trimmedComment); this.setState({ comment: '', }); @@ -106,6 +103,6 @@ class ReportHistoryCompose extends React.Component { ); } } -ReportHistoryCompose.propTypes = propTypes; +ReportActionCompose.propTypes = propTypes; -export default ReportHistoryCompose; +export default ReportActionCompose; diff --git a/src/page/home/report/ReportHistoryFragmentPropTypes.js b/src/page/home/report/ReportActionFragmentPropTypes.js similarity index 90% rename from src/page/home/report/ReportHistoryFragmentPropTypes.js rename to src/page/home/report/ReportActionFragmentPropTypes.js index 3dfdb0d8c085..51788260a798 100644 --- a/src/page/home/report/ReportHistoryFragmentPropTypes.js +++ b/src/page/home/report/ReportActionFragmentPropTypes.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; export default PropTypes.shape({ - // The type of the history fragment. Used to render a corresponding component + // The type of the action item fragment. Used to render a corresponding component type: PropTypes.string.isRequired, // The text content of the fragment. diff --git a/src/page/home/report/ReportActionItem.js b/src/page/home/report/ReportActionItem.js new file mode 100644 index 000000000000..cf9407c77ecd --- /dev/null +++ b/src/page/home/report/ReportActionItem.js @@ -0,0 +1,41 @@ +import React from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import ReportActionItemSingle from './ReportActionItemSingle'; +import ReportActionPropTypes from './ReportActionPropTypes'; +import ReportActionItemGrouped from './ReportActionItemGrouped'; + +const propTypes = { + // All the data of the action item + action: PropTypes.shape(ReportActionPropTypes).isRequired, + + // Should the comment have the appearance of being grouped with the previous comment? + displayAsGroup: PropTypes.bool.isRequired, +}; + +class ReportActionItem extends React.Component { + shouldComponentUpdate(nextProps) { + // This component should only render if the action's sequenceNumber or displayAsGroup props change + return nextProps.displayAsGroup !== this.props.displayAsGroup + || !_.isEqual(nextProps.action, this.props.action); + } + + render() { + const {action, displayAsGroup} = this.props; + if (action.actionName !== 'ADDCOMMENT') { + return null; + } + + return ( + + {!displayAsGroup && } + {displayAsGroup && } + + ); + } +} + +ReportActionItem.propTypes = propTypes; + +export default ReportActionItem; diff --git a/src/page/home/report/ReportHistoryItemDate.js b/src/page/home/report/ReportActionItemDate.js similarity index 70% rename from src/page/home/report/ReportHistoryItemDate.js rename to src/page/home/report/ReportActionItemDate.js index 5c0f63888623..7e31d510d10f 100644 --- a/src/page/home/report/ReportHistoryItemDate.js +++ b/src/page/home/report/ReportActionItemDate.js @@ -9,13 +9,13 @@ const propTypes = { timestamp: PropTypes.number.isRequired, }; -const ReportHistoryItemDate = props => ( +const ReportActionItemDate = props => ( {DateUtils.timestampToDateTime(props.timestamp)} ); -ReportHistoryItemDate.propTypes = propTypes; -ReportHistoryItemDate.displayName = 'ReportHistoryItemDate'; +ReportActionItemDate.propTypes = propTypes; +ReportActionItemDate.displayName = 'ReportActionItemDate'; -export default ReportHistoryItemDate; +export default ReportActionItemDate; diff --git a/src/page/home/report/ReportHistoryItemFragment.js b/src/page/home/report/ReportActionItemFragment.js similarity index 91% rename from src/page/home/report/ReportHistoryItemFragment.js rename to src/page/home/report/ReportActionItemFragment.js index 160837922a7f..aa0b8703b983 100644 --- a/src/page/home/report/ReportHistoryItemFragment.js +++ b/src/page/home/report/ReportActionItemFragment.js @@ -2,7 +2,7 @@ import React from 'react'; import HTML from 'react-native-render-html'; import {Linking} from 'react-native'; import Str from '../../../lib/Str'; -import ReportHistoryFragmentPropTypes from './ReportHistoryFragmentPropTypes'; +import ReportActionFragmentPropTypes from './ReportActionFragmentPropTypes'; import styles, {webViewStyles} from '../../../style/StyleSheet'; import Text from '../../../components/Text'; import AnchorForCommentsOnly from '../../../components/AnchorForCommentsOnly'; @@ -10,10 +10,10 @@ import {getAuthToken} from '../../../lib/Network'; const propTypes = { // The message fragment needing to be displayed - fragment: ReportHistoryFragmentPropTypes.isRequired, + fragment: ReportActionFragmentPropTypes.isRequired, }; -class ReportHistoryItemFragment extends React.PureComponent { +class ReportActionItemFragment extends React.PureComponent { constructor(props) { super(props); @@ -100,7 +100,7 @@ class ReportHistoryItemFragment extends React.PureComponent { } } -ReportHistoryItemFragment.propTypes = propTypes; -ReportHistoryItemFragment.displayName = 'ReportHistoryItemFragment'; +ReportActionItemFragment.propTypes = propTypes; +ReportActionItemFragment.displayName = 'ReportActionItemFragment'; -export default ReportHistoryItemFragment; +export default ReportActionItemFragment; diff --git a/src/page/home/report/ReportActionItemGrouped.js b/src/page/home/report/ReportActionItemGrouped.js new file mode 100644 index 000000000000..3ed21a4de5f7 --- /dev/null +++ b/src/page/home/report/ReportActionItemGrouped.js @@ -0,0 +1,30 @@ +import React from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import ReportActionPropTypes from './ReportActionPropTypes'; +import ReportActionItemMessage from './ReportActionItemMessage'; +import styles from '../../../style/StyleSheet'; + +const propTypes = { + // All the data of the action + action: PropTypes.shape(ReportActionPropTypes).isRequired, +}; + +class ReportActionItemGrouped extends React.PureComponent { + render() { + const {action} = this.props; + return ( + + + + + + + + ); + } +} + +ReportActionItemGrouped.propTypes = propTypes; + +export default ReportActionItemGrouped; diff --git a/src/page/home/report/ReportActionItemMessage.js b/src/page/home/report/ReportActionItemMessage.js new file mode 100644 index 000000000000..7566e149750f --- /dev/null +++ b/src/page/home/report/ReportActionItemMessage.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import ReportActionItemFragment from './ReportActionItemFragment'; +import ReportActionPropTypes from './ReportActionPropTypes'; + +const propTypes = { + // The report action + action: PropTypes.shape(ReportActionPropTypes).isRequired, +}; + +const ReportActionItemMessage = ({action}) => ( + <> + {_.map(_.compact(action.message), fragment => ( + + ))} + +); + +ReportActionItemMessage.propTypes = propTypes; +ReportActionItemMessage.displayName = 'ReportActionItemMessage'; + +export default ReportActionItemMessage; diff --git a/src/page/home/report/ReportActionItemSingle.js b/src/page/home/report/ReportActionItemSingle.js new file mode 100644 index 000000000000..abe3beeb690d --- /dev/null +++ b/src/page/home/report/ReportActionItemSingle.js @@ -0,0 +1,55 @@ +import React from 'react'; +import {View, Image} from 'react-native'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import ReportActionPropTypes from './ReportActionPropTypes'; +import ReportActionItemMessage from './ReportActionItemMessage'; +import ReportActionItemFragment from './ReportActionItemFragment'; +import styles from '../../../style/StyleSheet'; +import CONST from '../../../CONST'; +import ReportActionItemDate from './ReportActionItemDate'; + +const propTypes = { + // All the data of the action + action: PropTypes.shape(ReportActionPropTypes).isRequired, +}; + +class ReportActionItemSingle extends React.PureComponent { + render() { + const {action} = this.props; + const avatarUrl = action.automatic + ? `${CONST.CLOUDFRONT_URL}/images/icons/concierge_2019.svg` + : action.avatar; + return ( + + + + + + + + + {action.person.map(fragment => ( + + + + ))} + + + + + + + + + + ); + } +} + +ReportActionItemSingle.propTypes = propTypes; + +export default ReportActionItemSingle; diff --git a/src/page/home/report/ReportHistoryPropsTypes.js b/src/page/home/report/ReportActionPropTypes.js similarity index 56% rename from src/page/home/report/ReportHistoryPropsTypes.js rename to src/page/home/report/ReportActionPropTypes.js index a393fa3a9a8d..84f216010caf 100644 --- a/src/page/home/report/ReportHistoryPropsTypes.js +++ b/src/page/home/report/ReportActionPropTypes.js @@ -1,13 +1,13 @@ import PropTypes from 'prop-types'; -import HistoryFragmentPropTypes from './ReportHistoryFragmentPropTypes'; +import ReportActionFragmentPropTypes from './ReportActionFragmentPropTypes'; export default { // Name of the action e.g. ADDCOMMENT actionName: PropTypes.string.isRequired, // Person who created the action - person: PropTypes.arrayOf(HistoryFragmentPropTypes).isRequired, + person: PropTypes.arrayOf(ReportActionFragmentPropTypes).isRequired, // ID of the report action sequenceNumber: PropTypes.number.isRequired, @@ -15,6 +15,6 @@ export default { // Unix timestamp timestamp: PropTypes.number.isRequired, - // report history message - message: PropTypes.arrayOf(HistoryFragmentPropTypes).isRequired, + // report action message + message: PropTypes.arrayOf(ReportActionFragmentPropTypes).isRequired, }; diff --git a/src/page/home/report/ReportHistoryView.js b/src/page/home/report/ReportActionsView.js similarity index 66% rename from src/page/home/report/ReportHistoryView.js rename to src/page/home/report/ReportActionsView.js index da9595c99b93..0c241ed98fa8 100644 --- a/src/page/home/report/ReportHistoryView.js +++ b/src/page/home/report/ReportActionsView.js @@ -6,40 +6,55 @@ import lodashGet from 'lodash.get'; import Text from '../../../components/Text'; import Ion from '../../../lib/Ion'; import withIon from '../../../components/withIon'; -import {fetchHistory, updateLastReadActionID} from '../../../lib/actions/Report'; +import {fetchActions, updateLastReadActionID} from '../../../lib/actions/Report'; import IONKEYS from '../../../IONKEYS'; -import ReportHistoryItem from './ReportHistoryItem'; +import ReportActionItem from './ReportActionItem'; import styles from '../../../style/StyleSheet'; import {withRouter} from '../../../lib/Router'; -import ReportHistoryPropsTypes from './ReportHistoryPropsTypes'; +import ReportActionPropTypes from './ReportActionPropTypes'; import compose from '../../../lib/compose'; const propTypes = { + // These are from withRouter + // eslint-disable-next-line react/forbid-prop-types + match: PropTypes.object.isRequired, + // The ID of the report actions will be created for reportID: PropTypes.number.isRequired, /* Ion Props */ - // Array of report history items for this report - reportHistory: PropTypes.PropTypes.objectOf(PropTypes.shape(ReportHistoryPropsTypes)), + // Array of report actions for this report + reportActions: PropTypes.PropTypes.objectOf(PropTypes.shape(ReportActionPropTypes)), }; const defaultProps = { - reportHistory: {}, + reportActions: {}, }; -class ReportHistoryView extends React.Component { +class ReportActionsView extends React.Component { constructor(props) { super(props); this.recordlastReadActionID = _.debounce(this.recordlastReadActionID.bind(this), 1000, true); this.scrollToListBottom = this.scrollToListBottom.bind(this); + this.recordMaxAction = this.recordMaxAction.bind(this); } componentDidMount() { this.keyboardEvent = Keyboard.addListener('keyboardDidShow', this.scrollToListBottom); } + componentDidUpdate(prevProps) { + const isReportVisible = this.props.reportID === this.props.match.params.reportID; + + // When the number of actions change, wait three seconds, then record the max action + // This will make the unread indicator go away if you receive comments in the same chat you're looking at + if (isReportVisible && _.size(prevProps.reportActions) !== _.size(this.props.reportActions)) { + setTimeout(this.recordMaxAction, 3000); + } + } + componentWillUnmount() { this.keyboardEvent.remove(); } @@ -51,21 +66,21 @@ class ReportHistoryView extends React.Component { * Also checks to ensure that the comment is not too old to * be considered part of the same comment * - * @param {Number} historyItemIndex - index of the comment item in state to check + * @param {Number} actionIndex - index of the comment item in state to check * * @return {Boolean} */ // eslint-disable-next-line - isConsecutiveHistoryItemMadeByPreviousActor(historyItemIndex) { - const reportHistory = lodashGet(this.props, 'reportHistory', {}); + isConsecutiveActionMadeByPreviousActor(actionIndex) { + const reportActions = lodashGet(this.props, 'reportActions', {}); // This is the created action and the very first action so it cannot be a consecutive comment. - if (historyItemIndex === 0) { + if (actionIndex === 0) { return false; } - const previousAction = reportHistory[historyItemIndex - 1]; - const currentAction = reportHistory[historyItemIndex]; + const previousAction = reportActions[actionIndex - 1]; + const currentAction = reportActions[actionIndex]; // It's OK for there to be no previous action, and in that case, false will be returned // so that the comment isn't grouped @@ -91,8 +106,8 @@ class ReportHistoryView extends React.Component { * action when scrolled */ recordMaxAction() { - const reportHistory = lodashGet(this.props, 'reportHistory', {}); - const maxVisibleSequenceNumber = _.chain(reportHistory) + const reportActions = lodashGet(this.props, 'reportActions', {}); + const maxVisibleSequenceNumber = _.chain(reportActions) .pluck('sequenceNumber') .max() .value(); @@ -122,18 +137,18 @@ class ReportHistoryView extends React.Component { /** * 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 items in our history have changed since it was last rendered, then + * items have been rendered. If the number of actions has changed since it was last rendered, then * scroll the list to the end. */ scrollToListBottom() { - if (this.historyListElement) { - this.historyListElement.scrollToEnd({animated: false}); + if (this.actionListElement) { + this.actionListElement.scrollToEnd({animated: false}); } this.recordMaxAction(); } render() { - if (!_.size(this.props.reportHistory)) { + if (!_.size(this.props.reportActions)) { return ( Be the first person to comment! @@ -144,17 +159,17 @@ class ReportHistoryView extends React.Component { return ( { - this.historyListElement = el; + this.actionListElement = el; }} onContentSizeChange={this.scrollToListBottom} bounces={false} contentContainerStyle={[styles.chatContentScrollView]} > - {_.chain(this.props.reportHistory).sortBy('sequenceNumber').map((item, index) => ( - ( + )).value()} @@ -162,18 +177,18 @@ class ReportHistoryView extends React.Component { } } -ReportHistoryView.propTypes = propTypes; -ReportHistoryView.defaultProps = defaultProps; +ReportActionsView.propTypes = propTypes; +ReportActionsView.defaultProps = defaultProps; -const key = `${IONKEYS.REPORT_HISTORY}_%DATAFROMPROPS%`; +const key = `${IONKEYS.REPORT_ACTIONS}_%DATAFROMPROPS%`; export default compose( withRouter, withIon({ - reportHistory: { + reportActions: { key, - loader: fetchHistory, + loader: fetchActions, loaderParams: ['%DATAFROMPROPS%'], pathForProps: 'reportID', }, }), -)(ReportHistoryView); +)(ReportActionsView); diff --git a/src/page/home/report/ReportHistoryItem.js b/src/page/home/report/ReportHistoryItem.js deleted file mode 100644 index 4ed1949243c8..000000000000 --- a/src/page/home/report/ReportHistoryItem.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import ReportHistoryItemSingle from './ReportHistoryItemSingle'; -import ReportHistoryPropsTypes from './ReportHistoryPropsTypes'; -import ReportHistoryItemGrouped from './ReportHistoryItemGrouped'; - -const propTypes = { - // All the data of the history item - historyItem: PropTypes.shape(ReportHistoryPropsTypes).isRequired, - - // Should the comment have the appearance of being grouped with the previous comment? - displayAsGroup: PropTypes.bool.isRequired, -}; - -class ReportHistoryItem extends React.Component { - shouldComponentUpdate(nextProps) { - // This component should only render if the history item's sequenceNumber or displayAsGroup props change - return nextProps.displayAsGroup !== this.props.displayAsGroup - || !_.isEqual(nextProps.historyItem, this.props.historyItem); - } - - render() { - const {historyItem, displayAsGroup} = this.props; - if (historyItem.actionName !== 'ADDCOMMENT') { - return null; - } - - return ( - - {!displayAsGroup && } - {displayAsGroup && } - - ); - } -} - -ReportHistoryItem.propTypes = propTypes; - -export default ReportHistoryItem; diff --git a/src/page/home/report/ReportHistoryItemGrouped.js b/src/page/home/report/ReportHistoryItemGrouped.js deleted file mode 100644 index 1ba792322df5..000000000000 --- a/src/page/home/report/ReportHistoryItemGrouped.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import PropTypes from 'prop-types'; -import ReportHistoryPropsTypes from './ReportHistoryPropsTypes'; -import ReportHistoryItemMessage from './ReportHistoryItemMessage'; -import styles from '../../../style/StyleSheet'; - -const propTypes = { - // All the data of the history item - historyItem: PropTypes.shape(ReportHistoryPropsTypes).isRequired, -}; - -class ReportHistoryItemGrouped extends React.PureComponent { - render() { - const {historyItem} = this.props; - return ( - - - - - - - - ); - } -} - -ReportHistoryItemGrouped.propTypes = propTypes; - -export default ReportHistoryItemGrouped; diff --git a/src/page/home/report/ReportHistoryItemMessage.js b/src/page/home/report/ReportHistoryItemMessage.js deleted file mode 100644 index be2aa04f691d..000000000000 --- a/src/page/home/report/ReportHistoryItemMessage.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import ReportHistoryItemFragment from './ReportHistoryItemFragment'; -import ReportHistoryPropsTypes from './ReportHistoryPropsTypes'; - -const propTypes = { - // The report history item - historyItem: PropTypes.shape(ReportHistoryPropsTypes).isRequired, -}; - -const ReportHistoryItemMessage = ({historyItem}) => ( - <> - {_.map(_.compact(historyItem.message), fragment => ( - - ))} - -); - -ReportHistoryItemMessage.propTypes = propTypes; -ReportHistoryItemMessage.displayName = 'ReportHistoryItemMessage'; - -export default ReportHistoryItemMessage; diff --git a/src/page/home/report/ReportHistoryItemSingle.js b/src/page/home/report/ReportHistoryItemSingle.js deleted file mode 100644 index 44538c468c20..000000000000 --- a/src/page/home/report/ReportHistoryItemSingle.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import {View, Image} from 'react-native'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import ReportHistoryPropsTypes from './ReportHistoryPropsTypes'; -import ReportHistoryItemMessage from './ReportHistoryItemMessage'; -import ReportHistoryItemFragment from './ReportHistoryItemFragment'; -import styles from '../../../style/StyleSheet'; -import CONST from '../../../CONST'; -import ReportHistoryItemDate from './ReportHistoryItemDate'; - -const propTypes = { - // All the data of the history item - historyItem: PropTypes.shape(ReportHistoryPropsTypes).isRequired, -}; - -class ReportHistoryItemSingle extends React.PureComponent { - render() { - const {historyItem} = this.props; - const avatarUrl = historyItem.automatic - ? `${CONST.CLOUDFRONT_URL}/images/icons/concierge_2019.svg` - : historyItem.avatar; - return ( - - - - - - - - - {historyItem.person.map(fragment => ( - - - - ))} - - - - - - - - - - ); - } -} - -ReportHistoryItemSingle.propTypes = propTypes; - -export default ReportHistoryItemSingle; diff --git a/src/page/home/report/ReportView.js b/src/page/home/report/ReportView.js index 6c9aa2bd5728..6478ca8244d5 100644 --- a/src/page/home/report/ReportView.js +++ b/src/page/home/report/ReportView.js @@ -1,9 +1,9 @@ import React from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; -import ReportHistoryView from './ReportHistoryView'; -import ReportHistoryCompose from './ReportHistoryCompose'; -import {addHistoryItem} from '../../../lib/actions/Report'; +import ReportActionView from './ReportActionsView'; +import ReportActionCompose from './ReportActionCompose'; +import {addAction} from '../../../lib/actions/Report'; import KeyboardSpacer from '../../../components/KeyboardSpacer'; import styles from '../../../style/StyleSheet'; @@ -24,12 +24,11 @@ class ReportView extends React.PureComponent { const shouldShowComposeForm = this.props.isActiveReport; return ( - + {shouldShowComposeForm && ( - addAction(this.props.reportID, text)} /> )} diff --git a/src/page/home/sidebar/ChatSwitcherView.js b/src/page/home/sidebar/ChatSwitcherView.js index 66598d0f6e96..f2208a3bee86 100644 --- a/src/page/home/sidebar/ChatSwitcherView.js +++ b/src/page/home/sidebar/ChatSwitcherView.js @@ -8,7 +8,7 @@ import Str from '../../../lib/Str'; import KeyboardShortcut from '../../../lib/KeyboardShortcut'; import ChatSwitcherList from './ChatSwitcherList'; import ChatSwitcherSearchForm from './ChatSwitcherSearchForm'; -import {fetchChatReport} from '../../../lib/actions/Report'; +import {fetchOrCreateChatReport} from '../../../lib/actions/Report'; const propTypes = { // A method that is triggered when the TextInput gets focus @@ -125,7 +125,7 @@ class ChatSwitcherView extends React.Component { */ fetchChatReportAndRedirect(option) { Ion.get(IONKEYS.MY_PERSONAL_DETAILS, 'login') - .then(currentLogin => fetchChatReport([currentLogin, option.login])) + .then(currentLogin => fetchOrCreateChatReport([currentLogin, option.login])) .then(reportID => Ion.set(IONKEYS.APP_REDIRECT_TO, `/${reportID}`)); this.reset(); } diff --git a/src/page/home/sidebar/SidebarBottom.js b/src/page/home/sidebar/SidebarBottom.js index 4bb589f5aa87..bc6901ffca9f 100644 --- a/src/page/home/sidebar/SidebarBottom.js +++ b/src/page/home/sidebar/SidebarBottom.js @@ -52,7 +52,7 @@ const SidebarBottom = ({myPersonalDetails, isOffline, insets}) => { diff --git a/src/page/home/sidebar/SidebarLink.js b/src/page/home/sidebar/SidebarLink.js index ade838375d66..46b44dcf35ed 100644 --- a/src/page/home/sidebar/SidebarLink.js +++ b/src/page/home/sidebar/SidebarLink.js @@ -14,7 +14,7 @@ const propTypes = { reportID: PropTypes.number.isRequired, // The name of the report to use as the text for this link - reportName: PropTypes.string.isRequired, + reportName: PropTypes.string, // These are from withRouter // eslint-disable-next-line react/forbid-prop-types @@ -31,6 +31,7 @@ const propTypes = { const defaultProps = { isUnread: false, + reportName: '', }; const SidebarLink = (props) => { diff --git a/src/page/home/sidebar/SidebarLinks.js b/src/page/home/sidebar/SidebarLinks.js index a9d8af337f58..1af2ef40dd67 100644 --- a/src/page/home/sidebar/SidebarLinks.js +++ b/src/page/home/sidebar/SidebarLinks.js @@ -89,7 +89,9 @@ class SidebarLinks extends React.Component { Chats - {_.map(reportsToDisplay, report => ( + {/* A report will not have a report name if it hasn't been fetched from the server yet */} + {/* so nothing is rendered */} + {_.map(reportsToDisplay, report => report.reportName && (