diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6649a33fe15e..d2b3031220f1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -242,6 +242,7 @@ const ONYXKEYS = { POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', REPORT: 'report_', + REPORT_METADATA: 'reportMetadata_', REPORT_ACTIONS: 'reportActions_', REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', @@ -380,6 +381,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; + [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportAction; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: string; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; diff --git a/src/components/ExceededCommentLength.js b/src/components/ExceededCommentLength.js index 7c9ec4d2db25..9c785cec0395 100644 --- a/src/components/ExceededCommentLength.js +++ b/src/components/ExceededCommentLength.js @@ -63,5 +63,6 @@ ExceededCommentLength.displayName = 'ExceededCommentLength'; export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, + initialValue: '', }, })(ExceededCommentLength); diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.js b/src/components/Reactions/ReportActionItemEmojiReactions.js index c1e1764ed9f1..e72c9d9381fa 100644 --- a/src/components/Reactions/ReportActionItemEmojiReactions.js +++ b/src/components/Reactions/ReportActionItemEmojiReactions.js @@ -13,7 +13,7 @@ import EmojiReactionsPropTypes from './EmojiReactionsPropTypes'; import Tooltip from '../Tooltip'; import ReactionTooltipContent from './ReactionTooltipContent'; import * as EmojiUtils from '../../libs/EmojiUtils'; -import ReportScreenContext from '../../pages/home/ReportScreenContext'; +import {ReactionListContext} from '../../pages/home/ReportScreenContext'; const propTypes = { emojiReactions: EmojiReactionsPropTypes, @@ -41,8 +41,9 @@ const defaultProps = { }; function ReportActionItemEmojiReactions(props) { - const {reactionListRef} = useContext(ReportScreenContext); + const reactionListRef = useContext(ReactionListContext); const popoverReactionListAnchors = useRef({}); + let totalReactionCount = 0; // Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 1da348bb067b..1e484b172d3f 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -121,9 +121,11 @@ export default compose( withOnyx({ taskReport: { key: ({taskReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, + initialValue: {}, }, personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, + initialValue: {}, }, }), )(TaskPreview); diff --git a/src/components/ReportActionsSkeletonView/index.js b/src/components/ReportActionsSkeletonView/index.js index 2fe7e590afef..6bdc993c2055 100644 --- a/src/components/ReportActionsSkeletonView/index.js +++ b/src/components/ReportActionsSkeletonView/index.js @@ -1,13 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {View} from 'react-native'; +import {View, Dimensions} from 'react-native'; import SkeletonViewLines from './SkeletonViewLines'; import CONST from '../../CONST'; const propTypes = { - /** Height of the container component */ - containerHeight: PropTypes.number.isRequired, - /** Whether to animate the skeleton view */ shouldAnimate: PropTypes.bool, }; @@ -18,7 +15,7 @@ const defaultProps = { function ReportActionsSkeletonView(props) { // Determines the number of content items based on container height - const possibleVisibleContentItems = Math.ceil(props.containerHeight / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT); + const possibleVisibleContentItems = Math.ceil(Dimensions.get('window').height / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT); const skeletonViewLines = []; for (let index = 0; index < possibleVisibleContentItems; index++) { const iconIndex = (index + 1) % 4; diff --git a/src/components/withLocalize.js b/src/components/withLocalize.js index 6c588698ce9d..5ce1b0bc6d74 100755 --- a/src/components/withLocalize.js +++ b/src/components/withLocalize.js @@ -28,6 +28,9 @@ const withLocalizePropTypes = { /** Formats a datetime to local date and time string */ datetimeToCalendarTime: PropTypes.func.isRequired, + /** Updates date-fns internal locale */ + updateLocale: PropTypes.func.isRequired, + /** Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions */ formatPhoneNumber: PropTypes.func.isRequired, @@ -79,6 +82,7 @@ class LocaleContextProvider extends React.Component { numberFormat: this.numberFormat.bind(this), datetimeToRelative: this.datetimeToRelative.bind(this), datetimeToCalendarTime: this.datetimeToCalendarTime.bind(this), + updateLocale: this.updateLocale.bind(this), formatPhoneNumber: this.formatPhoneNumber.bind(this), fromLocaleDigit: this.fromLocaleDigit.bind(this), toLocaleDigit: this.toLocaleDigit.bind(this), @@ -122,6 +126,13 @@ class LocaleContextProvider extends React.Component { return DateUtils.datetimeToCalendarTime(this.props.preferredLocale, datetime, includeTimezone, lodashGet(this.props, 'currentUserPersonalDetails.timezone.selected'), isLowercase); } + /** + * Updates date-fns internal locale to the user preferredLocale + */ + updateLocale() { + DateUtils.setLocale(this.props.preferredLocale); + } + /** * @param {String} phoneNumber * @returns {String} diff --git a/src/hooks/useReportScrollManager/index.js b/src/hooks/useReportScrollManager/index.js index 0cf09146553c..9a3303504b92 100644 --- a/src/hooks/useReportScrollManager/index.js +++ b/src/hooks/useReportScrollManager/index.js @@ -1,8 +1,8 @@ import {useContext, useCallback} from 'react'; -import ReportScreenContext from '../../pages/home/ReportScreenContext'; +import {ActionListContext} from '../../pages/home/ReportScreenContext'; function useReportScrollManager() { - const {flatListRef} = useContext(ReportScreenContext); + const flatListRef = useContext(ActionListContext); /** * Scroll to the provided index. On non-native implementations we do not want to scroll when we are scrolling because diff --git a/src/hooks/useReportScrollManager/index.native.js b/src/hooks/useReportScrollManager/index.native.js index 35af064cb062..d44a40222ca5 100644 --- a/src/hooks/useReportScrollManager/index.native.js +++ b/src/hooks/useReportScrollManager/index.native.js @@ -1,8 +1,8 @@ import {useContext, useCallback} from 'react'; -import ReportScreenContext from '../../pages/home/ReportScreenContext'; +import {ActionListContext} from '../../pages/home/ReportScreenContext'; function useReportScrollManager() { - const {flatListRef} = useContext(ReportScreenContext); + const flatListRef = useContext(ActionListContext); /** * Scroll to the provided index. diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index 5c0171067870..a9ec4a3fd35a 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -386,6 +386,7 @@ const DateUtils = { subtractMillisecondsFromDateTime, getDateStringFromISOTimestamp, getStatusUntilDate, + setLocale, isToday, isTomorrow, isYesterday, diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js new file mode 100644 index 000000000000..24f855645870 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js @@ -0,0 +1,121 @@ +import {useEffect} from 'react'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; +import * as ReportUtils from '../../ReportUtils'; +import reportPropTypes from '../../../pages/reportPropTypes'; +import {withNavigationPropTypes} from '../../../components/withNavigation'; +import * as App from '../../actions/App'; +import usePermissions from '../../../hooks/usePermissions'; +import CONST from '../../../CONST'; +import Navigation from '../Navigation'; + +const propTypes = { + /** Available reports that would be displayed in this navigator */ + reports: PropTypes.objectOf(reportPropTypes), + + /** The policies which the user has access to */ + policies: PropTypes.objectOf( + PropTypes.shape({ + /** The policy name */ + name: PropTypes.string, + + /** The type of the policy */ + type: PropTypes.string, + }), + ), + + isFirstTimeNewExpensifyUser: PropTypes.bool, + + /** Navigation route context info provided by react navigation */ + route: PropTypes.shape({ + /** Route specific parameters used on this screen */ + params: PropTypes.shape({ + /** If the admin room should be opened */ + openOnAdminRoom: PropTypes.bool, + + /** The ID of the report this screen should display */ + reportID: PropTypes.string, + }), + }).isRequired, + + ...withNavigationPropTypes, +}; + +const defaultProps = { + reports: {}, + policies: {}, + isFirstTimeNewExpensifyUser: false, +}; + +/** + * Get the most recently accessed report for the user + * + * @param {Object} reports + * @param {Boolean} ignoreDefaultRooms + * @param {Object} policies + * @param {Boolean} isFirstTimeNewExpensifyUser + * @param {Boolean} openOnAdminRoom + * @returns {Number} + */ +const getLastAccessedReportID = (reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom) => { + // If deeplink url is of an attachment, we should show the report that the attachment comes from. + const currentRoute = Navigation.getActiveRoute(); + const matches = CONST.REGEX.ATTACHMENT_ROUTE.exec(currentRoute); + const reportID = lodashGet(matches, 1, null); + if (reportID) { + return reportID; + } + + const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom); + + return lodashGet(lastReport, 'reportID'); +}; + +// This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params +function ReportScreenIDSetter({route, reports, policies, isFirstTimeNewExpensifyUser, navigation}) { + const {canUseDefaultRooms} = usePermissions(); + + useEffect(() => { + // Don't update if there is a reportID in the params already + if (lodashGet(route, 'params.reportID', null)) { + App.confirmReadyToOpenApp(); + return; + } + + // If there is no reportID in route, try to find last accessed and use it for setParams + const reportID = getLastAccessedReportID(reports, !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, lodashGet(route, 'params.openOnAdminRoom', false)); + + // It's possible that reports aren't fully loaded yet + // in that case the reportID is undefined + if (reportID) { + navigation.setParams({reportID: String(reportID)}); + } else { + App.confirmReadyToOpenApp(); + } + }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser]); + + // The ReportScreen without the reportID set will display a skeleton + // until the reportID is loaded and set in the route param + return null; +} + +ReportScreenIDSetter.propTypes = propTypes; +ReportScreenIDSetter.defaultProps = defaultProps; +ReportScreenIDSetter.displayName = 'ReportScreenIDSetter'; + +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + allowStaleData: true, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + allowStaleData: true, + }, + isFirstTimeNewExpensifyUser: { + key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, + initialValue: false, + }, +})(ReportScreenIDSetter); diff --git a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js index f1743e1a2269..767bd9793ac2 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js +++ b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js @@ -1,36 +1,10 @@ -import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; -import lodashGet from 'lodash/get'; -import {withOnyx} from 'react-native-onyx'; - -import ONYXKEYS from '../../../ONYXKEYS'; - -import ReportScreen from '../../../pages/home/ReportScreen'; -import * as ReportUtils from '../../ReportUtils'; -import reportPropTypes from '../../../pages/reportPropTypes'; +import React from 'react'; import {withNavigationPropTypes} from '../../../components/withNavigation'; -import * as App from '../../actions/App'; -import usePermissions from '../../../hooks/usePermissions'; -import CONST from '../../../CONST'; -import Navigation from '../Navigation'; +import ReportScreen from '../../../pages/home/ReportScreen'; +import ReportScreenIDSetter from './ReportScreenIDSetter'; const propTypes = { - /** Available reports that would be displayed in this navigator */ - reports: PropTypes.objectOf(reportPropTypes), - - /** The policies which the user has access to */ - policies: PropTypes.objectOf( - PropTypes.shape({ - /** The policy name */ - name: PropTypes.string, - - /** The type of the policy */ - type: PropTypes.string, - }), - ), - - isFirstTimeNewExpensifyUser: PropTypes.bool, - /** Navigation route context info provided by react navigation */ route: PropTypes.shape({ /** Route specific parameters used on this screen */ @@ -46,82 +20,24 @@ const propTypes = { ...withNavigationPropTypes, }; -const defaultProps = { - reports: {}, - policies: {}, - isFirstTimeNewExpensifyUser: false, -}; - -/** - * Get the most recently accessed report for the user - * - * @param {Object} reports - * @param {Boolean} [ignoreDefaultRooms] - * @param {Object} policies - * @param {Boolean} isFirstTimeNewExpensifyUser - * @param {Boolean} openOnAdminRoom - * @returns {Number} - */ -const getLastAccessedReportID = (reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom) => { - // If deeplink url is of an attachment, we should show the report that the attachment comes from. - const currentRoute = Navigation.getActiveRoute(); - const matches = CONST.REGEX.ATTACHMENT_ROUTE.exec(currentRoute); - const reportID = lodashGet(matches, 1, null); - if (reportID) { - return reportID; - } - - const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom); - - return lodashGet(lastReport, 'reportID'); -}; +const defaultProps = {}; -// This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params function ReportScreenWrapper(props) { - const {canUseDefaultRooms} = usePermissions(); - - useEffect(() => { - // Don't update if there is a reportID in the params already - if (lodashGet(props.route, 'params.reportID', null)) { - App.confirmReadyToOpenApp(); - return; - } - - // If there is no reportID in route, try to find last accessed and use it for setParams - const reportID = getLastAccessedReportID( - props.reports, - !canUseDefaultRooms, - props.policies, - props.isFirstTimeNewExpensifyUser, - lodashGet(props.route, 'params.openOnAdminRoom', false), - ); - - // It's possible that props.reports aren't fully loaded yet - // in that case the reportID is undefined - if (reportID) { - props.navigation.setParams({reportID: String(reportID)}); - } else { - App.confirmReadyToOpenApp(); - } - }, [props.route, props.navigation, props.reports, canUseDefaultRooms, props.policies, props.isFirstTimeNewExpensifyUser]); - // The ReportScreen without the reportID set will display a skeleton // until the reportID is loaded and set in the route param - return ; + return ( + <> + + + + ); } ReportScreenWrapper.propTypes = propTypes; ReportScreenWrapper.defaultProps = defaultProps; ReportScreenWrapper.displayName = 'ReportScreenWrapper'; -export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - isFirstTimeNewExpensifyUser: { - key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, - }, -})(ReportScreenWrapper); +export default ReportScreenWrapper; diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 4d50a1cd6a68..0dda39517d3c 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -122,7 +122,10 @@ function NavigationRoot(props) { if (!state) { return; } - updateCurrentReportID(state); + // Performance optimization to avoid context consumers to delay first render + setTimeout(() => { + updateCurrentReportID(state); + }, 0); parseAndLogRoute(state); animateStatusBarBackgroundColor(); }; diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index e42ef1ac4823..fcce909c5582 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -310,7 +310,7 @@ function createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs) { workspaceMembersChats.onyxFailureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${optimisticReport.reportID}`, value: { isLoadingReportActions: false, }, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 2de53293853a..b92862c5b5e1 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -450,43 +450,63 @@ function reportActionsExist(reportID) { * @param {Array} participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it */ function openReport(reportID, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { - const optimisticReportData = { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: reportActionsExist(reportID) - ? {} - : { - isLoadingReportActions: true, - isLoadingMoreReportActions: false, - reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), - }, - }; - const reportSuccessData = { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingReportActions: false, - pendingFields: { - createChat: null, + const optimisticReportData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: reportActionsExist(reportID) + ? {} + : { + reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingReportActions: true, + isLoadingMoreReportActions: false, }, - errorFields: { - createChat: null, + }, + ]; + + const reportSuccessData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + pendingFields: { + createChat: null, + }, + errorFields: { + createChat: null, + }, + isOptimisticReport: false, }, - isOptimisticReport: false, }, - }; - const reportFailureData = { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingReportActions: false, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingReportActions: false, + }, }, - }; + ]; + + const reportFailureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingReportActions: false, + }, + }, + ]; const onyxData = { - optimisticData: [optimisticReportData], - successData: [reportSuccessData], - failureData: [reportFailureData], + optimisticData: optimisticReportData, + successData: reportSuccessData, + failureData: reportFailureData, }; const params = { @@ -503,17 +523,17 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p // If we open an exist report, but it is not present in Onyx yet, we should change the method to set for this report // and we need data to be available when we navigate to the chat page if (_.isEmpty(ReportUtils.getReport(reportID))) { - optimisticReportData.onyxMethod = Onyx.METHOD.SET; + onyxData.optimisticData[0].onyxMethod = Onyx.METHOD.SET; } // If we are creating a new report, we need to add the optimistic report data and a report action if (!_.isEmpty(newReportObject)) { // Change the method to set for new reports because it doesn't exist yet, is faster, // and we need the data to be available when we navigate to the chat page - optimisticReportData.onyxMethod = Onyx.METHOD.SET; - optimisticReportData.value = { + onyxData.optimisticData[0].onyxMethod = Onyx.METHOD.SET; + onyxData.optimisticData[0].value = { reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - ...optimisticReportData.value, + ...onyxData.optimisticData[0].value, ...newReportObject, pendingFields: { createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, @@ -696,17 +716,23 @@ function reconnect(reportID) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, value: { isLoadingReportActions: true, isLoadingMoreReportActions: false, - reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), }, }, ], successData: [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, value: { isLoadingReportActions: false, }, @@ -715,7 +741,7 @@ function reconnect(reportID) { failureData: [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, value: { isLoadingReportActions: false, }, diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 477d063c1747..b7d9a923d0bf 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -25,7 +25,6 @@ import reportPropTypes from '../reportPropTypes'; import ONYXKEYS from '../../ONYXKEYS'; import ThreeDotsMenu from '../../components/ThreeDotsMenu'; import * as Task from '../../libs/actions/Task'; -import reportActionPropTypes from './report/reportActionPropTypes'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import PinButton from '../../components/PinButton'; import TaskHeaderActionButton from '../../components/TaskHeaderActionButton'; @@ -45,33 +44,22 @@ const propTypes = { /** Onyx Props */ parentReport: reportPropTypes, - /** The details about the account that the user is signing in with */ - account: PropTypes.shape({ - /** URL to the assigned guide's appointment booking calendar */ - guideCalendarLink: PropTypes.string, - }), + /** URL to the assigned guide's appointment booking calendar */ + guideCalendarLink: PropTypes.string, /** Current user session */ session: PropTypes.shape({ accountID: PropTypes.number, }), - /** The report actions from the parent report */ - // TO DO: Replace with HOC https://github.com/Expensify/App/issues/18769. - // eslint-disable-next-line react/no-unused-prop-types - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), - ...windowDimensionsPropTypes, ...withLocalizePropTypes, }; const defaultProps = { personalDetails: {}, - parentReportActions: {}, report: null, - account: { - guideCalendarLink: null, - }, + guideCalendarLink: null, parentReport: {}, session: { accountID: 0, @@ -93,13 +81,12 @@ function HeaderView(props) { const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData); const isConcierge = ReportUtils.hasSingleParticipant(props.report) && _.contains(participants, CONST.ACCOUNT_ID.CONCIERGE); const isAutomatedExpensifyAccount = ReportUtils.hasSingleParticipant(props.report) && ReportUtils.hasAutomatedExpensifyAccountIDs(participants); - const guideCalendarLink = lodashGet(props.account, 'guideCalendarLink'); const parentReportAction = ReportActionsUtils.getParentReportAction(props.report); const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(props.report, parentReportAction); // We hide the button when we are chatting with an automated Expensify account since it's not possible to contact // these users via alternative means. It is possible to request a call with Concierge so we leave the option for them. - const shouldShowCallButton = (isConcierge && guideCalendarLink) || (!isAutomatedExpensifyAccount && !isTaskReport); + const shouldShowCallButton = (isConcierge && props.guideCalendarLink) || (!isAutomatedExpensifyAccount && !isTaskReport); const threeDotMenuItems = []; if (isTaskReport && !isCanceledTaskReport) { const canModifyTask = Task.canModifyTask(props.report, props.session.accountID); @@ -222,7 +209,7 @@ function HeaderView(props) { {shouldShowCallButton && ( )} @@ -247,17 +234,10 @@ export default compose( withWindowDimensions, withLocalize, withOnyx({ - account: { + guideCalendarLink: { key: ONYXKEYS.ACCOUNT, - selector: (account) => - account && { - guideCalendarLink: account.guideCalendarLink, - primaryLogin: account.primaryLogin, - }, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - canEvict: false, + selector: (account) => (account && account.guideCalendarLink) || null, + initialValue: null, }, parentReport: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || report.reportID}`, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 999622b9a22e..c30a8c7ed4a8 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -16,15 +16,15 @@ import * as ReportUtils from '../../libs/ReportUtils'; import ReportActionsView from './report/ReportActionsView'; import ReportActionsSkeletonView from '../../components/ReportActionsSkeletonView'; import reportActionPropTypes from './report/reportActionPropTypes'; -import useNetwork from '../../hooks/useNetwork'; -import useWindowDimensions from '../../hooks/useWindowDimensions'; -import useLocalize from '../../hooks/useLocalize'; import compose from '../../libs/compose'; import Visibility from '../../libs/Visibility'; +import useWindowDimensions from '../../hooks/useWindowDimensions'; +import useLocalize from '../../hooks/useLocalize'; import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import ReportFooter from './report/ReportFooter'; import Banner from '../../components/Banner'; import reportPropTypes from '../reportPropTypes'; +import reportMetadataPropTypes from '../reportMetadataPropTypes'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import withViewportOffsetTop, {viewportOffsetTopPropTypes} from '../../components/withViewportOffsetTop'; import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; @@ -33,7 +33,7 @@ import getIsReportFullyVisible from '../../libs/getIsReportFullyVisible'; import MoneyRequestHeader from '../../components/MoneyRequestHeader'; import MoneyReportHeader from '../../components/MoneyReportHeader'; import * as ComposerActions from '../../libs/actions/Composer'; -import ReportScreenContext from './ReportScreenContext'; +import {ActionListContext, ReactionListContext} from './ReportScreenContext'; import TaskHeaderActionButton from '../../components/TaskHeaderActionButton'; import DragAndDropProvider from '../../components/DragAndDrop/Provider'; import usePrevious from '../../hooks/usePrevious'; @@ -59,6 +59,9 @@ const propTypes = { /** The report currently being looked at */ report: reportPropTypes, + /** The report metadata loading states */ + reportMetadata: reportMetadataPropTypes, + /** Array of report actions for this report */ reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), @@ -85,6 +88,9 @@ const propTypes = { /** All of the personal details for everyone */ personalDetails: PropTypes.objectOf(personalDetailsPropType), + /** Onyx function that marks the component ready for hydration */ + markReadyForHydration: PropTypes.func, + /** Whether user is leaving the current report */ userLeavingStatus: PropTypes.bool, @@ -97,7 +103,10 @@ const defaultProps = { reportActions: [], report: { hasOutstandingIOU: false, + }, + reportMetadata: { isLoadingReportActions: false, + isLoadingMoreReportActions: false, }, isComposerFullSize: false, betas: [], @@ -105,6 +114,7 @@ const defaultProps = { accountManagerReportID: null, userLeavingStatus: false, personalDetails: {}, + markReadyForHydration: null, ...withCurrentReportIDDefaultProps, }; @@ -133,9 +143,11 @@ function ReportScreen({ betas, route, report, + reportMetadata, reportActions, accountManagerReportID, personalDetails, + markReadyForHydration, policies, isSidebarLoaded, viewportOffsetTop, @@ -145,7 +157,6 @@ function ReportScreen({ currentReportID, }) { const {translate} = useLocalize(); - const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); const firstRenderRef = useRef(true); @@ -153,8 +164,6 @@ function ReportScreen({ const reactionListRef = useRef(); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - - const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); const [isBannerVisible, setIsBannerVisible] = useState(true); const reportID = getReportID(route); @@ -162,13 +171,13 @@ function ReportScreen({ const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(reportActions) && report.isLoadingReportActions; + const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); - const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails) || firstRenderRef.current; + const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails); const parentReportAction = ReportActionsUtils.getParentReportAction(report); const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); @@ -344,112 +353,112 @@ function ReportScreen({ } }, [report, didSubscribeToReportLeavingEvents, reportID]); + const onListLayout = useCallback(() => { + if (!markReadyForHydration) { + return; + } + + markReadyForHydration(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo( - () => (!_.isEmpty(report) && !isDefaultReport && !report.reportID && !isOptimisticDelete && !report.isLoadingReportActions && !isLoading && !userLeavingStatus) || shouldHideReport, + () => + (!firstRenderRef.current && + !_.isEmpty(report) && + !isDefaultReport && + !report.reportID && + !isOptimisticDelete && + !report.isLoadingReportActions && + !isLoading && + !userLeavingStatus) || + shouldHideReport, [report, isLoading, shouldHideReport, isDefaultReport, isOptimisticDelete, userLeavingStatus], ); return ( - - - + + - - {headerView} - {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( - - - - + + {headerView} + {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( + + + + + - - )} - - {Boolean(accountManagerReportID) && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( - - )} - - { - // Rounding this value for comparison because they can look like this: 411.9999694824219 - const newSkeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height); - - // The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it - // takes up so we can set the skeleton view container height. - if (newSkeletonViewContainerHeight === 0) { - return; - } - setSkeletonViewContainerHeight(newSkeletonViewContainerHeight); - }} - > - {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( - )} + + {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + + )} + + + {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( + + )} - {/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then - we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {(!isReportReadyForDisplay || isLoadingInitialReportActions || isLoading) && } + {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. + If we prevent rendering the report while they are loading then + we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} + {(!isReportReadyForDisplay || isLoadingInitialReportActions || isLoading) && } - {isReportReadyForDisplay && ( - <> + {isReportReadyForDisplay ? ( - - )} - - {!isReportReadyForDisplay && ( - - )} - - - - - + ) : ( + + )} + + + + + + ); } @@ -460,35 +469,50 @@ ReportScreen.displayName = 'ReportScreen'; export default compose( withViewportOffsetTop, withCurrentReportID, - withOnyx({ - isSidebarLoaded: { - key: ONYXKEYS.IS_SIDEBAR_LOADED, + withOnyx( + { + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, + }, + reportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: ReportActionsUtils.getSortedReportActionsForDisplay, + }, + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, + allowStaleData: true, + }, + reportMetadata: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`, + initialValue: { + isLoadingReportActions: false, + isLoadingMoreReportActions: false, + }, + }, + isComposerFullSize: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, + initialValue: false, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + allowStaleData: true, + }, + accountManagerReportID: { + key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, + initialValue: null, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + userLeavingStatus: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, + initialValue: false, + }, }, - reportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, - }, - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, - }, - isComposerFullSize: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - accountManagerReportID: { - key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - userLeavingStatus: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, - }, - }), + true, + ), )(ReportScreen); diff --git a/src/pages/home/ReportScreenContext.js b/src/pages/home/ReportScreenContext.js index 2f79d6ae9432..1e8d30cf7585 100644 --- a/src/pages/home/ReportScreenContext.js +++ b/src/pages/home/ReportScreenContext.js @@ -1,4 +1,6 @@ import {createContext} from 'react'; -const ReportScreenContext = createContext(); -export default ReportScreenContext; +const ActionListContext = createContext(); +const ReactionListContext = createContext(); + +export {ActionListContext, ReactionListContext}; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js index da5dc326d421..09f9d368bdcc 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js @@ -68,5 +68,6 @@ SilentCommentUpdater.displayName = 'SilentCommentUpdater'; export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, + initialValue: '', }, })(SilentCommentUpdater); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 645f7d32abdb..fe1dcf248f90 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -64,8 +64,8 @@ import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils'; import ReportActionItemBasicMessage from './ReportActionItemBasicMessage'; import * as store from '../../../libs/actions/ReimbursementAccount/store'; import * as BankAccounts from '../../../libs/actions/BankAccounts'; +import {ReactionListContext} from '../ReportScreenContext'; import usePrevious from '../../../hooks/usePrevious'; -import ReportScreenContext from '../ReportScreenContext'; import Permissions from '../../../libs/Permissions'; import RenderHTML from '../../../components/RenderHTML'; import ReportAttachmentsContext from './ReportAttachmentsContext'; @@ -130,7 +130,7 @@ function ReportActionItem(props) { const [isContextMenuActive, setIsContextMenuActive] = useState(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); const [isHidden, setIsHidden] = useState(false); const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); - const {reactionListRef} = useContext(ReportScreenContext); + const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); const textInputRef = useRef(); const popoverAnchorRef = useRef(); @@ -676,6 +676,7 @@ export default compose( withOnyx({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, }, iouReport: { key: ({action}) => { @@ -686,6 +687,7 @@ export default compose( }, emojiReactions: { key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, + initialValue: {}, }, }), )( @@ -700,6 +702,7 @@ export default compose( _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && _.isEqual(prevProps.action, nextProps.action) && _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && + _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) && lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') && lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') && diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index f3f40d34a0f5..0163a7ff2b4f 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -32,6 +32,9 @@ const propTypes = { /** The ID of the most recent IOU report action connected with the shown report */ mostRecentIOUReportActionID: PropTypes.string, + /** The report metadata loading states */ + isLoadingReportActions: PropTypes.bool, + /** Are we loading more report actions? */ isLoadingMoreReportActions: PropTypes.bool, @@ -61,6 +64,7 @@ const defaultProps = { personalDetails: {}, onScroll: () => {}, mostRecentIOUReportActionID: '', + isLoadingReportActions: false, isLoadingMoreReportActions: false, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -96,6 +100,8 @@ function isMessageUnread(message, lastReadTime) { function ReportActionsList({ report, + isLoadingReportActions, + isLoadingMoreReportActions, sortedReportActions, windowHeight, onScroll, @@ -117,10 +123,11 @@ function ReportActionsList({ const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); const reportActionSize = useRef(sortedReportActions.length); + const firstRenderRef = useRef(true); - // Considering that renderItem is enclosed within a useCallback, marking it as "read" twice will retain the value as "true," preventing the useCallback from re-executing. - // However, if we create and listen to an object, it will lead to a new useCallback execution. - const [messageManuallyMarked, setMessageManuallyMarked] = useState({read: false}); + // This state is used to force a re-render when the user manually marks a message as unread + // by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before + const [messageManuallyMarkedUnread, setMessageManuallyMarkedUnread] = useState(0); const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(false); const animatedStyles = useAnimatedStyle(() => ({ opacity: opacity.value, @@ -129,7 +136,6 @@ function ReportActionsList({ useEffect(() => { opacity.value = withTiming(1, {duration: 100}); }, [opacity]); - const [skeletonViewHeight, setSkeletonViewHeight] = useState(0); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because @@ -167,14 +173,14 @@ function ReportActionsList({ useEffect(() => { const didManuallyMarkReportAsUnread = report.lastReadTime < DateUtils.getDBTime() && ReportUtils.isUnread(report); - if (!didManuallyMarkReportAsUnread) { - setMessageManuallyMarked({read: false}); + if (didManuallyMarkReportAsUnread) { + // Clearing the current unread marker so that it can be recalculated + setCurrentUnreadMarker(null); + setMessageManuallyMarkedUnread(new Date().getTime()); return; } - // Clearing the current unread marker so that it can be recalculated - setCurrentUnreadMarker(null); - setMessageManuallyMarked({read: true}); + setMessageManuallyMarkedUnread(0); // We only care when a new lastReadTime is set in the report // eslint-disable-next-line react-hooks/exhaustive-deps @@ -281,7 +287,7 @@ function ReportActionsList({ const isCurrentMessageUnread = isMessageUnread(reportAction, report.lastReadTime); shouldDisplayNewMarker = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime); - if (!messageManuallyMarked.read) { + if (!messageManuallyMarkedUnread) { shouldDisplayNewMarker = shouldDisplayNewMarker && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); } const canDisplayMarker = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; @@ -305,7 +311,7 @@ function ReportActionsList({ /> ); }, - [report, hasOutstandingIOU, sortedReportActions, mostRecentIOUReportActionID, messageManuallyMarked, shouldHideThreadDividerLine, currentUnreadMarker], + [report, hasOutstandingIOU, sortedReportActions, mostRecentIOUReportActionID, messageManuallyMarkedUnread, shouldHideThreadDividerLine, currentUnreadMarker], ); // Native mobile does not render updates flatlist the changes even though component did update called. @@ -314,6 +320,36 @@ function ReportActionsList({ const hideComposer = ReportUtils.shouldDisableWriteActions(report); const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; + const renderFooter = useCallback(() => { + // Skip this hook on the first render, as we are not sure if more actions are going to be loaded + // Therefore showing the skeleton on footer might be misleading + if (firstRenderRef.current) { + firstRenderRef.current = false; + return null; + } + + if (isLoadingMoreReportActions) { + return ; + } + + // Make sure the oldest report action loaded is not the first. This is so we do not show the + // skeleton view above the created action in a newly generated optimistic chat or one with not + // that many comments. + const lastReportAction = _.last(sortedReportActions) || {}; + if (isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + return ; + } + + return null; + }, [isLoadingMoreReportActions, isLoadingReportActions, sortedReportActions, isOffline]); + + const onLayoutInner = useCallback( + (event) => { + onLayout(event); + }, + [onLayout], + ); + return ( <> { - if (report.isLoadingMoreReportActions) { - return ; - } - - // Make sure the oldest report action loaded is not the first. This is so we do not show the - // skeleton view above the created action in a newly generated optimistic chat or one with not - // that many comments. - const lastReportAction = _.last(sortedReportActions) || {}; - if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ( - - ); - } - - return null; - }} + ListFooterComponent={renderFooter} keyboardShouldPersistTaps="handled" - onLayout={(event) => { - setSkeletonViewHeight(event.nativeEvent.layout.height); - onLayout(event); - }} + onLayout={onLayoutInner} onScroll={trackVerticalScrolling} extraData={extraData} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a694c4996438..f58c6644cd47 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -19,7 +19,7 @@ import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; import reportPropTypes from '../../reportPropTypes'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import getIsReportFullyVisible from '../../../libs/getIsReportFullyVisible'; -import ReportScreenContext from '../ReportScreenContext'; +import {ReactionListContext} from '../ReportScreenContext'; const propTypes = { /** The report currently being looked at */ @@ -28,6 +28,12 @@ const propTypes = { /** Array of report actions for this report */ reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + /** The report metadata loading states */ + isLoadingReportActions: PropTypes.bool, + + /** The report actions are loading more data */ + isLoadingMoreReportActions: PropTypes.bool, + /** Whether the composer is full size */ /* eslint-disable-next-line react/no-unused-prop-types */ isComposerFullSize: PropTypes.bool.isRequired, @@ -51,13 +57,13 @@ const propTypes = { const defaultProps = { reportActions: [], policy: null, + isLoadingReportActions: false, + isLoadingMoreReportActions: false, }; function ReportActionsView(props) { - const context = useContext(ReportScreenContext); - useCopySelectionHelper(); - + const reactionListRef = useContext(ReactionListContext); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const hasCachedActions = useRef(_.size(props.reportActions) > 0); @@ -138,7 +144,7 @@ function ReportActionsView(props) { */ const loadMoreChats = () => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. - if (props.report.isLoadingMoreReportActions) { + if (props.isLoadingMoreReportActions) { return; } @@ -185,11 +191,12 @@ function ReportActionsView(props) { onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} - isLoadingMoreReportActions={props.report.isLoadingMoreReportActions} + isLoadingReportActions={props.isLoadingReportActions} + isLoadingMoreReportActions={props.isLoadingMoreReportActions} loadMoreChats={loadMoreChats} policy={props.policy} /> - + ); } @@ -215,11 +222,11 @@ function arePropsEqual(oldProps, newProps) { return false; } - if (oldProps.report.isLoadingMoreReportActions !== newProps.report.isLoadingMoreReportActions) { + if (oldProps.isLoadingMoreReportActions !== newProps.isLoadingMoreReportActions) { return false; } - if (oldProps.report.isLoadingReportActions !== newProps.report.isLoadingReportActions) { + if (oldProps.isLoadingReportActions !== newProps.isLoadingReportActions) { return false; } diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index 8d92c09b7a6e..51a8490162e5 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -11,6 +11,7 @@ import ArchivedReportFooter from '../../../components/ArchivedReportFooter'; import compose from '../../../libs/compose'; import ONYXKEYS from '../../../ONYXKEYS'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; +import useNetwork from '../../../hooks/useNetwork'; import styles from '../../../styles/styles'; import variables from '../../../styles/variables'; import reportActionPropTypes from './reportActionPropTypes'; @@ -25,9 +26,6 @@ const propTypes = { /** Report actions for the current report */ reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), - /** Offline status */ - isOffline: PropTypes.bool.isRequired, - /** Callback fired when the comment is submitted */ onSubmitComment: PropTypes.func, @@ -53,7 +51,8 @@ const defaultProps = { }; function ReportFooter(props) { - const chatFooterStyles = {...styles.chatFooter, minHeight: !props.isOffline ? CONST.CHAT_FOOTER_MIN_HEIGHT : 0}; + const {isOffline} = useNetwork(); + const chatFooterStyles = {...styles.chatFooter, minHeight: !isOffline ? CONST.CHAT_FOOTER_MIN_HEIGHT : 0}; const isArchivedRoom = ReportUtils.isArchivedRoom(props.report); const isAnonymousUser = Session.isAnonymousUser(); @@ -102,5 +101,6 @@ export default compose( withWindowDimensions, withOnyx({ shouldShowComposeInput: {key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT}, + initialValue: false, }), )(ReportFooter); diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 4de649c7eb49..db97f712d65f 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -74,6 +74,7 @@ export default compose( withOnyx({ userTypingStatuses: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, + initialValue: {}, }, }), )(ReportTypingIndicator); diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.js b/src/pages/home/report/withReportAndReportActionOrNotFound.js index 9bf3e73e761c..b4346504b327 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.js +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.js @@ -6,6 +6,7 @@ import getComponentDisplayName from '../../../libs/getComponentDisplayName'; import NotFoundPage from '../../ErrorPage/NotFoundPage'; import ONYXKEYS from '../../../ONYXKEYS'; import reportPropTypes from '../../reportPropTypes'; +import reportMetadataPropTypes from '../../reportMetadataPropTypes'; import reportActionPropTypes from './reportActionPropTypes'; import FullscreenLoadingIndicator from '../../../components/FullscreenLoadingIndicator'; import * as ReportUtils from '../../../libs/ReportUtils'; @@ -23,6 +24,9 @@ export default function (WrappedComponent) { /** The report currently being looked at */ report: reportPropTypes, + /** The report metadata */ + reportMetadata: reportMetadataPropTypes, + /** Array of report actions for this report */ reportActions: PropTypes.shape(reportActionPropTypes), @@ -62,6 +66,10 @@ export default function (WrappedComponent) { forwardedRef: () => {}, reportActions: {}, report: {}, + reportMetadata: { + isLoadingReportActions: false, + isLoadingMoreReportActions: false, + }, policies: {}, betas: [], isLoadingReportData: true, @@ -94,7 +102,7 @@ export default function (WrappedComponent) { // Perform all the loading checks const isLoadingReport = props.isLoadingReportData && (_.isEmpty(props.report) || !props.report.reportID); - const isLoadingReportAction = _.isEmpty(props.reportActions) || (props.report.isLoadingReportActions && _.isEmpty(getReportAction())); + const isLoadingReportAction = _.isEmpty(props.reportActions) || (props.reportMetadata.isLoadingReportActions && _.isEmpty(getReportAction())); const shouldHideReport = !isLoadingReport && (_.isEmpty(props.report) || !props.report.reportID || !ReportUtils.canAccessReport(props.report, props.policies, props.betas)); if ((isLoadingReport || isLoadingReportAction) && !shouldHideReport) { @@ -135,6 +143,9 @@ export default function (WrappedComponent) { report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, }, + reportMetadata: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${route.params.reportID}`, + }, isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 9ff9cc261af4..984654c6b506 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import React from 'react'; -import {View} from 'react-native'; +import {View, InteractionManager} from 'react-native'; import _ from 'underscore'; import PropTypes from 'prop-types'; import styles from '../../../styles/styles'; @@ -77,6 +77,13 @@ class SidebarLinks extends React.PureComponent { SidebarUtils.setIsSidebarLoadedReady(); this.isSidebarLoaded = true; + // Eagerly set the locale on date-fns, it helps navigating to the report screen faster + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + this.props.updateLocale(); + }); + }); + let modal = {}; this.unsubscribeOnyxModal = onyxSubscribe({ key: ONYXKEYS.MODAL, @@ -134,11 +141,7 @@ class SidebarLinks extends React.PureComponent { // or when clicking the active LHN row on large screens // or when continuously clicking different LHNs, only apply to small screen // since getTopmostReportId always returns on other devices - if ( - this.props.isCreateMenuOpen || - (!this.props.isSmallScreenWidth && this.props.isActiveReport(option.reportID)) || - (this.props.isSmallScreenWidth && Navigation.getTopmostReportId()) - ) { + if (this.props.isCreateMenuOpen || option.reportID === Navigation.getTopmostReportId() || (this.props.isSmallScreenWidth && this.props.isActiveReport(option.reportID))) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); @@ -195,4 +198,5 @@ class SidebarLinks extends React.PureComponent { SidebarLinks.propTypes = propTypes; SidebarLinks.defaultProps = defaultProps; export default compose(withLocalize, withWindowDimensions)(SidebarLinks); + export {basePropTypes}; diff --git a/src/pages/reportMetadataPropTypes.js b/src/pages/reportMetadataPropTypes.js new file mode 100644 index 000000000000..a75d71aef7b3 --- /dev/null +++ b/src/pages/reportMetadataPropTypes.js @@ -0,0 +1,9 @@ +import PropTypes from 'prop-types'; + +export default PropTypes.shape({ + /** Are we loading more report actions? */ + isLoadingMoreReportActions: PropTypes.bool, + + /** Flag to check if the report actions data are loading */ + isLoadingReportActions: PropTypes.bool, +}); diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index da90e0a4ac5c..a2c41b5a8147 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -13,12 +13,6 @@ export default PropTypes.shape({ /** List of icons for report participants */ icons: PropTypes.arrayOf(avatarPropTypes), - /** Are we loading more report actions? */ - isLoadingMoreReportActions: PropTypes.bool, - - /** Flag to check if the report actions data are loading */ - isLoadingReportActions: PropTypes.bool, - /** Whether the user is not an admin of policyExpenseChat chat */ isOwnPolicyExpenseChat: PropTypes.bool, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 88caa683305d..46e51fe41238 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -12,12 +12,6 @@ type Report = { /** List of icons for report participants */ icons?: OnyxCommon.Icon[]; - /** Are we loading more report actions? */ - isLoadingMoreReportActions?: boolean; - - /** Flag to check if the report actions data are loading */ - isLoadingReportActions?: boolean; - /** Whether the user is not an admin of policyExpenseChat chat */ isOwnPolicyExpenseChat?: boolean; diff --git a/src/types/onyx/ReportMetadata.ts b/src/types/onyx/ReportMetadata.ts new file mode 100644 index 000000000000..3e389c8cff4f --- /dev/null +++ b/src/types/onyx/ReportMetadata.ts @@ -0,0 +1,9 @@ +type ReportMetadata = { + /** Are we loading more report actions? */ + isLoadingMoreReportActions?: boolean; + + /** Flag to check if the report actions data are loading */ + isLoadingReportActions?: boolean; +}; + +export default ReportMetadata; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 815689efdaaf..e50925e7adf2 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -36,6 +36,7 @@ import PolicyMember from './PolicyMember'; import Policy from './Policy'; import PolicyCategory from './PolicyCategory'; import Report from './Report'; +import ReportMetadata from './ReportMetadata'; import ReportAction from './ReportAction'; import ReportActionReactions from './ReportActionReactions'; import SecurityGroup from './SecurityGroup'; @@ -84,6 +85,7 @@ export type { Policy, PolicyCategory, Report, + ReportMetadata, ReportAction, ReportActionReactions, SecurityGroup,