Skip to content
89 changes: 52 additions & 37 deletions src/lib/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Ion.connect({
// Keeps track of the max sequence number for each report
const reportMaxSequenceNumbers = {};

// Keeps track of the last read for each report
const lastReadActionIDs = {};

// List of reportIDs that we define in .env
const configReportIDs = CONFIG.REPORT_IDS.split(',').map(Number);

Expand All @@ -51,29 +54,27 @@ const configReportIDs = CONFIG.REPORT_IDS.split(',').map(Number);
* @param {object} report
* @returns {boolean}
*/
function hasUnreadActions(report) {
function getUnreadActionCount(report) {
const usersLastReadActionID = lodashGet(report, [
'reportNameValuePairs',
`lastReadActionID_${currentUserAccountID}`,
]);

// Save the lastReadActionID locally so we can access this later
lastReadActionIDs[report.reportID] = usersLastReadActionID;

if (report.reportActionList.length === 0) {
return false;
return 0;
}

if (!usersLastReadActionID) {
return true;
}

// Find the most recent sequence number from the report actions
const maxSequenceNumber = reportMaxSequenceNumbers[report.reportID];

if (!maxSequenceNumber) {
return false;
return report.reportActionList.length;
}

// There are unread items if the last one the user has read is less than the highest sequence number we have
return usersLastReadActionID < maxSequenceNumber;
// There are unread items if the last one the user has read is less
// than the highest sequence number we have
const unreadActionCount = report.reportActionList.length - usersLastReadActionID;
return Math.max(0, unreadActionCount);
}

/**
Expand All @@ -91,8 +92,9 @@ function getSimplifiedReportObject(report) {
reportID: report.reportID,
reportName: report.reportName,
reportNameValuePairs: report.reportNameValuePairs,
isUnread: hasUnreadActions(report),
unreadActionCount: getUnreadActionCount(report),
pinnedReport: configReportIDs.includes(report.reportID),
maxSequenceNumber: report.reportActionList.length,
};
}

Expand Down Expand Up @@ -145,9 +147,8 @@ function fetchChatReportsByIDs(chatList) {
PersonalDetails.getForEmails(emails.join(','));
}


// Process the reports and store them in Ion
const ionPromises = _.map(fetchedReports, (report) => {
_.each(fetchedReports, (report) => {
const newReport = getSimplifiedReportObject(report);

if (lodashGet(report, 'reportNameValuePairs.type') === 'chat') {
Expand All @@ -158,7 +159,7 @@ function fetchChatReportsByIDs(chatList) {
Ion.merge(`${IONKEYS.COLLECTION.REPORT}${report.reportID}`, newReport);
});

return Promise.all(ionPromises);
return _.map(fetchedReports, report => report.reportID);
});
}

Expand All @@ -169,16 +170,14 @@ function fetchChatReportsByIDs(chatList) {
* @param {object} reportAction
*/
function updateReportWithNewAction(reportID, reportAction) {
const previousMaxSequenceNumber = reportMaxSequenceNumbers[reportID] || 0;
const newMaxSequenceNumber = reportAction.sequenceNumber;
const hasNewSequenceNumber = newMaxSequenceNumber > previousMaxSequenceNumber;

// 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.COLLECTION.REPORT}${reportID}`, {
reportID,
isUnread: hasNewSequenceNumber,
unreadActionCount: newMaxSequenceNumber - (lastReadActionIDs[reportID] || 0),
maxSequenceNumber: reportAction.sequenceNumber,
});

Expand Down Expand Up @@ -269,31 +268,24 @@ function fetchActions(reportID) {
}

/**
* Get all of our reports
* Get all of our hardcoded reports
*
* @param {boolean} shouldRedirectToFirstReport this is set to false when the network reconnect
* code runs
* @param {boolean} shouldFetchActions whether or not the actions of the reports should also be fetched
*
* @returns {Promise}
*/
function fetchAll(shouldRedirectToFirstReport = true, shouldFetchActions = false) {
let fetchedReports;

function fetchHardcodedReports(shouldRedirectToFirstReport) {
// Request each report one at a time to allow individual reports to fail if access to it is prevented by Auth
const reportFetchPromises = _.map(configReportIDs, reportID => API.get({
returnValueList: 'reportStuff',
reportIDList: reportID,
shouldLoadOptionalKeys: true,
}));

// Chat reports need to be fetched separately than the reports hard-coded in the config
// files. The promise for fetching them is added to the array of promises here so
// that both types of reports (chat reports and hard-coded reports) are fetched in
// parallel
reportFetchPromises.push(fetchChatReports());

promiseAllSettled(reportFetchPromises)
return promiseAllSettled(reportFetchPromises)
.then((data) => {
fetchedReports = _.compact(_.map(data, (promiseResult) => {
const 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', {});

Expand All @@ -316,11 +308,32 @@ function fetchAll(shouldRedirectToFirstReport = true, shouldFetchActions = false
// 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)
Ion.merge(`${IONKEYS.COLLECTION.REPORT}${report.reportID}`, getSimplifiedReportObject(report));
});

if (shouldFetchActions) {
console.debug(`[RECONNECT] Fetching report actions for report ${report.reportID}`);
fetchActions(report.reportID);
}
return _.map(fetchedReports, report => report.reportID);
});
}

/**
* Get all of our reports
*
* @param {boolean} shouldRedirectToFirstReport this is set to false when the network reconnect
* code runs
* @param {boolean} shouldFetchActions whether or not the actions of the reports should also be fetched
*/
function fetchAll(shouldRedirectToFirstReport = true, shouldFetchActions = false) {
Promise.all([
fetchChatReports(),
fetchHardcodedReports(shouldRedirectToFirstReport)
])
.then((promiseResults) => {
if (!shouldFetchActions) {
return;
}

_.each(_.flatten(promiseResults), (reportID) => {
console.debug(`[RECONNECT] Fetching report actions for report ${reportID}`);
fetchActions(reportID);
});
});
}
Expand Down Expand Up @@ -446,9 +459,11 @@ function updateLastReadActionID(reportID, sequenceNumber) {
return;
}

lastReadActionIDs[reportID] = sequenceNumber;

// Update the lastReadActionID on the report optimistically
Ion.merge(`${IONKEYS.COLLECTION.REPORT}${reportID}`, {
isUnread: false,
unreadActionCount: 0,
reportNameValuePairs: {
[`lastReadActionID_${currentUserAccountID}`]: sequenceNumber,
}
Expand Down
2 changes: 1 addition & 1 deletion src/page/home/MainView.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class MainView extends Component {

const reportsToDisplay = _.filter(this.props.reports, report => (
report.pinnedReport
|| report.isUnread
|| report.unreadActionCount > 0
|| report.reportID === reportIDInUrl
));
return (
Expand Down
41 changes: 10 additions & 31 deletions src/page/home/sidebar/SidebarLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import Text from '../../../components/Text';
import {withRouter} from '../../../lib/Router';
import IONKEYS from '../../../IONKEYS';
import styles from '../../../style/StyleSheet';
import withIon from '../../../components/withIon';
import PressableLink from '../../../components/PressableLink';
import compose from '../../../lib/compose';

const propTypes = {
// The ID of the report for this link
Expand All @@ -16,36 +12,26 @@ const propTypes = {
// The name of the report to use as the text for this link
reportName: PropTypes.string,

// These are from withRouter
// eslint-disable-next-line react/forbid-prop-types
match: PropTypes.object.isRequired,

// Toggles the hamburger menu open and closed
onLinkClick: PropTypes.func.isRequired,

/* Ion Props */
// Does the report for this link have unread comments?
isUnread: PropTypes.bool,

// The report object for this link
report: PropTypes.shape({
// Does the report for this link have unread comments?
isUnread: PropTypes.bool,
}),
// Whether this is the report currently in view
isActiveReport: PropTypes.bool.isRequired,
};

const defaultProps = {
report: {
isUnread: false,
},
isUnread: false,
reportName: '',
};

const SidebarLink = (props) => {
const reportIDInUrl = parseInt(props.match.params.reportID, 10);
const isReportActive = reportIDInUrl === props.reportID;
const linkWrapperActiveStyle = isReportActive && styles.sidebarLinkWrapperActive;
const linkActiveStyle = isReportActive ? styles.sidebarLinkActive : styles.sidebarLink;
const textActiveStyle = isReportActive ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
const textActiveUnreadStyle = props.report.isUnread
const linkWrapperActiveStyle = props.isActiveReport && styles.sidebarLinkWrapperActive;
const linkActiveStyle = props.isActiveReport ? styles.sidebarLinkActive : styles.sidebarLink;
const textActiveStyle = props.isActiveReport ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
const textActiveUnreadStyle = props.isUnread
? [textActiveStyle, styles.sidebarLinkTextUnread] : [textActiveStyle];

return (
Expand All @@ -65,11 +51,4 @@ SidebarLink.displayName = 'SidebarLink';
SidebarLink.propTypes = propTypes;
SidebarLink.defaultProps = defaultProps;

export default compose(
withRouter,
withIon({
report: {
key: ({reportID}) => `${IONKEYS.COLLECTION.REPORT}${reportID}`,
},
}),
)(SidebarLink);
export default SidebarLink;
13 changes: 7 additions & 6 deletions src/page/home/sidebar/SidebarLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import Text from '../../../components/Text';
import SidebarLink from './SidebarLink';
import withIon from '../../../components/withIon';
import IONKEYS from '../../../IONKEYS';
import PageTitleUpdater from '../../../lib/PageTitleUpdater';
import ChatSwitcherView from './ChatSwitcherView';
import PageTitleUpdater from '../../../lib/PageTitleUpdater';
import SafeAreaInsetPropTypes from '../../SafeAreaInsetPropTypes';
import compose from '../../../lib/compose';
import {withRouter} from '../../../lib/Router';
Expand Down Expand Up @@ -37,7 +37,7 @@ const propTypes = {
reports: PropTypes.objectOf(PropTypes.shape({
reportID: PropTypes.number,
reportName: PropTypes.string,
isUnread: PropTypes.bool,
unreadActionCount: PropTypes.number,
})),
};
const defaultProps = {
Expand All @@ -54,16 +54,16 @@ class SidebarLinks extends React.Component {
}

render() {
const {reports, onLinkClick} = this.props;
const {onLinkClick} = this.props;
const reportIDInUrl = parseInt(this.props.match.params.reportID, 10);
const sortedReports = lodashOrderby(this.props.reports, ['pinnedReport', 'reportName'], ['desc', 'asc']);

// Filter the reports so that the only reports shown are pinned, unread, and the one matching the URL
// eslint-disable-next-line max-len
const reportsToDisplay = _.filter(sortedReports, report => (report.pinnedReport || report.isUnread || report.reportID === reportIDInUrl));
const reportsToDisplay = _.filter(sortedReports, report => (report.pinnedReport || (report.unreadActionCount > 0) || report.reportID === reportIDInUrl));

// Updates the page title to indicate there are unread reports
PageTitleUpdater(_.any(reports, report => report.isUnread));
PageTitleUpdater(_.any(sortedReports, report => report.unreadActionCount > 0));

// Update styles to hide the report links if they should not be visible
const sidebarLinksStyle = this.state.areReportLinksVisible
Expand Down Expand Up @@ -98,8 +98,9 @@ class SidebarLinks extends React.Component {
key={report.reportID}
reportID={report.reportID}
reportName={report.reportName}
isUnread={report.isUnread}
isUnread={report.unreadActionCount > 0}
onLinkClick={onLinkClick}
isActiveReport={report.reportID === reportIDInUrl}
/>
))}
</View>
Expand Down