From 5258cff5cf967d8fa06b3b29d29195a63d64b0f6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:50:57 +0200 Subject: [PATCH 001/124] update openReport --- src/components/ReportActionItem/MoneyRequestAction.js | 4 ++-- src/libs/actions/Report.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index b4a5e010b7a8..5ed47e4a244d 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -107,11 +107,11 @@ function MoneyRequestAction({ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(action, requestReportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, action.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, action.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport(childReportID); + Report.openReport({reportID: childReportID}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index a6e115fe5d9c..808bec8faee4 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -443,13 +443,14 @@ function reportActionsExist(reportID) { * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * * @param {String} reportID + * @param {String} reportActionID * @param {Array} participantLoginList The list of users that are included in a new chat, not including the user creating it * @param {Object} newReportObject The optimistic report object created when making a new chat, saved as optimistic data * @param {String} parentReportActionID The parent report action that a thread was created from (only passed for new threads) * @param {Boolean} isFromDeepLink Whether or not this report is being opened from a deep link * @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 = []) { +function openReport({reportID, reportActionID = ''}, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { const optimisticReportData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -512,6 +513,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p const params = { reportID, + reportActionID, emailList: participantLoginList ? participantLoginList.join(',') : '', accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '', parentReportActionID, From 5cd7e4c36cb1951901dcdf1fb16b8d907466a17a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:52:25 +0200 Subject: [PATCH 002/124] update the rest openReport calls --- src/pages/home/report/ContextMenu/ContextMenuActions.js | 4 ++-- .../home/report/withReportAndReportActionOrNotFound.js | 2 +- tests/actions/IOUTest.js | 8 ++++---- tests/actions/ReportTest.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 157ae66dc918..e19927bfb33b 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -287,11 +287,11 @@ export default [ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, reportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport(childReportID); + Report.openReport({reportID: childReportID}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.js b/src/pages/home/report/withReportAndReportActionOrNotFound.js index 47f499423d28..19bbf5b36e32 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.js +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.js @@ -97,7 +97,7 @@ export default function (WrappedComponent) { if (!props.isSmallScreenWidth || (!_.isEmpty(props.report) && !_.isEmpty(reportAction))) { return; } - Report.openReport(props.route.params.reportID); + Report.openReport({reportID: props.route.params.reportID}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.isSmallScreenWidth, props.route.params.reportID]); diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 3df3b137bab3..ef613c26020a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -1714,7 +1714,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID @@ -1780,7 +1780,7 @@ describe('actions/IOU', () => { thread = ReportUtils.buildTransactionThread(createIOUAction); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); jest.advanceTimersByTime(10); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); Onyx.connect({ @@ -1870,7 +1870,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); @@ -2083,7 +2083,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); const allReportActions = await new Promise((resolve) => { diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index 06d8304111cb..1968b4983e70 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -267,7 +267,7 @@ describe('actions/Report', () => { // When the user visits the report jest.advanceTimersByTime(10); currentTime = DateUtils.getDBTime(); - Report.openReport(REPORT_ID); + Report.openReport({reportID: REPORT_ID}); Report.readNewestAction(REPORT_ID); waitForBatchedUpdates(); return waitForBatchedUpdates(); From 65edb743531fa9c662388afd6f5db486375080a8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 30 Sep 2023 10:18:59 +0200 Subject: [PATCH 003/124] WIP linking utils --- src/libs/ReportActionsUtils.js | 160 +++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 67c44784eeb2..29cd6cd9cc54 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -212,6 +212,163 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal .value(); } +// /** +// * Given an object of reportActions, sorts them, and then adds the previousReportActionID to each item except the first. +// * @param {Object} reportActions +// * @returns {Array} +// */ +// function processReportActions(reportActions) { +// // TODO: +// // Separate new and sorted reportActions +// const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); +// const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); + +// // console.log( +// // 'getChat.Sort.N.0', +// // newReportActions.length, +// // newReportActions.map(({message}) => message[0].text), +// // ); +// // console.log( +// // 'getChat.Sort.N.0.0', +// // newReportActions.length, +// // newReportActions.map(({previousReportActionID}) => previousReportActionID), +// // ); +// // console.log( +// // 'getChat.Sort.S.0', +// // sortedReportActions.length, +// // sortedReportActions.map(({message}) => message[0].text), +// // ); +// // console.log( +// // 'getChat.Sort.S.0.0', +// // sortedReportActions.length, +// // sortedReportActions.map(({previousReportActionID}) => previousReportActionID), +// // ); +// // Sort the new reportActions +// const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); + +// // console.log( +// // 'getChat.SORT.SS', +// // JSON.stringify( +// // sortedNewReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); + +// // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first +// const processedReportActions = sortedNewReportActions.map((action, index) => { +// if (index === sortedNewReportActions.length - 1) { +// return action; // Return the first item as is +// } +// return { +// ...action, +// previousReportActionID: sortedNewReportActions[index + 1].reportActionID, +// }; +// }); + +// // console.log( +// // 'getChat.SORT.BEFORE', +// // JSON.stringify( +// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); +// if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { +// processedReportActions.pop(); +// } +// // console.log( +// // 'getChat.SORT.AFTER', +// // JSON.stringify( +// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); + +// // Determine the order of merging based on reportActionID values +// const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; +// const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; + +// // console.log('getChat.Sort.1', getSortedReportActionsForDisplay(reportActions).length, [...sortedReportActions, ...processedReportActions].length); +// // console.log('getChat.Sort.1.1', _.last(sortedReportActions), _.first(processedReportActions)); +// if (firstProcessedReportActionID > lastSortedReportActionID) { +// // console.log( +// // 'getChat.Sort.2', +// // [...sortedReportActions, ...processedReportActions].map(({message}) => message[0].text), +// // ); +// return [...sortedReportActions, ...processedReportActions]; +// } else { +// // console.log( +// // 'getChat.Sort.3', +// // [...processedReportActions, ...sortedReportActions].map(({message}) => message[0].text), +// // ); +// return [...processedReportActions, ...sortedReportActions]; +// } +// } + +// Usage: +// const reportActions = [ +// { reportActionID: '1' }, +// { reportActionID: '2', previousReportActionID: '1' }, +// { reportActionID: '3' } +// ]; +// const updatedActions = processReportActions(reportActions); +// console.log(updatedActions); + +function getRangeFromArrayByID(array, id) { + // without gaps + let index; + + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = 0; + } + + if (index === -1) { + return []; + } + + let startIndex = index; + let endIndex = index; + + // Move down the list and compare reportActionID with previousReportActionID + while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { + endIndex++; + } + + // Move up the list and compare previousReportActionID with reportActionID + while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) { + startIndex--; + } + + return array.slice(startIndex, endIndex + 1); + // return array.slice(startIndex, endIndex); +} + +function getSlicedRangeFromArrayByID(array, id) { + let index; + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = array.length - 1; + } + + if (index === -1) { + return []; + } + + // return array.slice(0, index+1); + return array.slice(index, array.length); +} + /** * Finds most recent IOU request action ID. * @@ -696,4 +853,7 @@ export { getAllReportActions, isReportActionAttachment, isNotifiableReportAction, + // processReportActions, + getRangeFromArrayByID, + getSlicedRangeFromArrayByID, }; From 03804dbf5e3a4ef33c1496ec185cdf69f5e3e697 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 2 Oct 2023 16:22:58 +0200 Subject: [PATCH 004/124] WIP navigation, useRouteChangeHandler --- src/libs/ReportUtils.js | 26 +++++ src/pages/home/ReportScreen.js | 105 +++++++++++++++-- src/pages/home/report/ReportActionsView.js | 125 ++++++++++++++++++--- 3 files changed, 232 insertions(+), 24 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c03858cb15f3..2be9e68ae451 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1,4 +1,5 @@ /* eslint-disable rulesdir/prefer-underscore-method */ +import {useEffect, useRef} from 'react'; import _ from 'underscore'; import {format, parseISO} from 'date-fns'; import Str from 'expensify-common/lib/str'; @@ -3654,6 +3655,30 @@ function getIOUReportActionDisplayMessage(reportAction) { return displayMessage; } +/** + * Hook that triggers a fetch when reportActionID appears while reportID stays the same. + * + * @param {string} reportID - The current reportID from the route. + * @param {string|null} reportActionID - The current reportActionID from the route or null. + * @param {function} triggerFetch - The function to be triggered on the condition. + */ +function useRouteChangeHandler(reportID = '', reportActionID = '', triggerFetch = () => {}) { + // Store the previous reportID and reportActionID + const previousReportIDRef = useRef(null); + const previousReportActionIDRef = useRef(null); + + useEffect(() => { + // Check if reportID is the same and reportActionID just appeared + if (reportID === previousReportIDRef.current && reportActionID && !previousReportActionIDRef.current) { + triggerFetch(); + } + + // Update refs for the next render + previousReportIDRef.current = reportID; + previousReportActionIDRef.current = reportActionID; + }, [reportID, reportActionID, triggerFetch]); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -3795,4 +3820,5 @@ export { hasMissingSmartscanFields, getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, + useRouteChangeHandler, }; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index aedc9247a21f..29cc51fab73a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,3 +1,4 @@ +/* eslint-disable rulesdir/prefer-underscore-method */ import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import {useFocusEffect} from '@react-navigation/native'; @@ -63,7 +64,7 @@ const propTypes = { reportMetadata: reportMetadataPropTypes, /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), /** Whether the composer is full size */ isComposerFullSize: PropTypes.bool, @@ -100,7 +101,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - reportActions: [], + sortedReportActions: [], report: { hasOutstandingIOU: false, }, @@ -139,13 +140,26 @@ const checkDefaultReport = (report) => report === defaultProps.report; function getReportID(route) { return String(lodashGet(route, 'params.reportID', null)); } +/** + * Get the currently viewed report ID as number + * + * @param {Object} route + * @param {Object} route.params + * @param {String} route.params.reportID + * @returns {String} + */ +function getReportActionID(route) { + console.log('get.ROUTE', route?.params); + return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; +} function ReportScreen({ betas, route, report, reportMetadata, - reportActions, + // reportActions, + sortedReportActions, accountManagerReportID, personalDetails, markReadyForHydration, @@ -156,6 +170,7 @@ function ReportScreen({ errors, userLeavingStatus, currentReportID, + updateCurrentReportID, }) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -165,9 +180,53 @@ function ReportScreen({ const reactionListRef = useRef(); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); + const {reportActionID, reportID} = getReportActionID(route); + const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + + const reportActions = useMemo(() => { + const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); + console.log('get.ROOT.reportActions', reportActionID, sortedReportActions.length, val.length); + return val; + }, [sortedReportActions, reportActionID]); + // const isReportActionArrayCatted = useMemo(() => sortedReportActions.length !== withoutGaps.length, [withoutGaps, sortedReportActions]); + + // const reportActions = useMemo(() => { + // //TODO: OpenReport, it means that we clicked on the link in current chat and we need to get a proper range of reportActions + // // console.log( + // // 'get.reportActions.initialSorted', + // // // sortedReportActions.length, + // // sortedReportActions.length, + // // !!reportActionID, + // // sortedReportActions.map((item) => { + // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; + // // }), + // // ); + // // // // console.log('get.reportActions.initialSorted', sortedReportActions.map((item) =>item?.previousReportActionID)); + // // console.log( + // // 'get.reportActions.withoutGaps', + // // // withoutGaps.length, + // // withoutGaps.length, + // // !!reportActionID, + // // withoutGaps.map((item) => { + // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; + // // }), + // // ); + // return withoutGaps; + // // return sortedReportActions; + // }, [sortedReportActions, reportActionID]); + + // const reportActionsBeforeAndIncludingLinked = useMemo(() => { + // if (reportActionID) { + // firstRenderRefI.current = false; + // return ReportActionsUtils.getSlicedRangeFromArrayByID(reportActions, reportActionID); + // } + // return null; + // }, [reportActions, reportActionID]); + + // const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); + // >>>>>>> Stashed changes const [isBannerVisible, setIsBannerVisible] = useState(true); - const reportID = getReportID(route); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -237,6 +296,8 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); + // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''})) + const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); @@ -249,11 +310,26 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route)) { + if (report.reportID && report.reportID === getReportID(route) && !reportActionID) { return; } - Report.openReport(reportIDFromPath); - }, [report.reportID, route]); + console.log('getChat.OPEN_REPORT', reportActionID); + Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''}); + }, [report.reportID, route, reportActionID]); + + // useEffect(() => { + // console.log('get.ROUTE.0', route); + // const {reportActionID} = getReportActionID(route); + // if (!reportActionID) return; + // fetchReportIfNeeded(); + // console.log('get.ROUTE.+++++++++', route); + // setLinkingToMessageTrigger(true); + // }, [route, fetchReportIfNeeded]); + // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => { + // console.log('getChat.OPEN_REPORT.000', reportActionID); + // fetchReportIfNeeded(); + // // updateCurrentReportID(getReportID(route)); + // }); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -283,7 +359,7 @@ function ReportScreen({ return; } - Report.openReport(report.reportID); + Report.openReport({reportID: report.reportID}); }); return () => unsubscribeVisibilityListener(); @@ -426,10 +502,15 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && !isFirstlyLoadingReportActions && !isLoading && ( + {/* {isReportReadyForDisplay && !isFirstlyLoadingReportActions && !isLoading && ( */} + {isReportReadyForDisplay && !isLoading && ( `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, + sortedReportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: ReportActionsUtils.getSortedReportActionsForDisplay, + // selector: ReportActionsUtils.processReportActions, + }, }, true, ), diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a0dee8abdb71..88e1dcef3e91 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,8 +1,8 @@ -import React, {useRef, useEffect, useContext, useMemo} from 'react'; +import React, {useRef, useEffect, useContext, useMemo, useState} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; -import {useIsFocused} from '@react-navigation/native'; +import {useIsFocused, useRoute} from '@react-navigation/native'; import * as Report from '../../../libs/actions/Report'; import reportActionPropTypes from './reportActionPropTypes'; import Timing from '../../../libs/actions/Timing'; @@ -65,15 +65,98 @@ const defaultProps = { isLoadingNewerReportActions: false, }; -function ReportActionsView(props) { +/** + * Get the currently viewed report ID as number + * + * @param {Object} route + * @param {Object} route.params + * @param {String} route.params.reportID + * @returns {String} + */ +function getReportActionID(route) { + return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; +} + +function ReportActionsView({reportActions: allReportActions, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); + const route = useRoute(); + const {reportActionID} = getReportActionID(route); + // const reportActionID = '' + + // const [canShowAllReports, setShowAllReports] = useState(false); + const testRef = useRef(false); const didLayout = useRef(false); + // const isFirstRender = useRef(true); const didSubscribeToReportTypingEvents = useRef(false); - const isFetchNewerWasCalled = useRef(false); - const hasCachedActions = useRef(_.size(props.reportActions) > 0); + const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); + const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + + const reportActionsBeforeAndIncludingLinked = useMemo(() => { + if (reportActionID && allReportActions?.length) { + return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); + } + return []; + }, [allReportActions, reportActionID]); + + const reportActions = useMemo(() => { + console.log( + 'get.reportActions.info|||', + '| reportActionID:', + reportActionID, + '| isFetchNewerWasCalled:', + isFetchNewerWasCalled, + '| isLinkingToMessage:', + isLinkingToMessage, + '| testRef.current:', + testRef.current, + '| reportActionsBeforeAndIncludingLinked:', + reportActionsBeforeAndIncludingLinked?.length, + '| allReportActions:', + allReportActions?.length, + ); + + if (!reportActionID || (!testRef.current && !isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { + console.log('get.reportActions.ALL||||', allReportActions.length); + return allReportActions; + } + console.log( + 'get.reportActions.CUT||||', + reportActionsBeforeAndIncludingLinked[0]?.message?.text || reportActionsBeforeAndIncludingLinked[0]?.message + ); + return reportActionsBeforeAndIncludingLinked; + }, [isFetchNewerWasCalled, allReportActions, reportActionsBeforeAndIncludingLinked, reportActionID, isLinkingToMessage, props.isLoadingInitialReportActions]); - const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + useEffect(() => { + console.log('get.ROUTE_CHANGED.triggered', route); + if (!reportActionID) { + return; + } + testRef.current = true; + setLinkingToMessageTrigger(true); + props.fetchReportIfNeeded(); + console.log('get.ROUTE_CHANGED.+++++++++'); + setTimeout(() => { + testRef.current = false; + setLinkingToMessageTrigger(false); + console.log('get.ROUTE_CHANGED.+++++++++FINISH', route); + }, 7000); + // setLinkingToMessageTrigger(true); + }, [route, props.fetchReportIfNeeded, reportActionID]); + + const isReportActionArrayCatted = useMemo(() => { + if (reportActions?.length !== allReportActions?.length && reportActionID) { + console.log('get.isReportActionArrayCatted.+++++++++'); + return true; + } + + console.log('get.isReportActionArrayCatted.----------'); + return false; + }, [reportActions, allReportActions, reportActionID, isFetchNewerWasCalled]); + + const hasCachedActions = useRef(_.size(reportActions) > 0); + + const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(reportActions)); const prevNetworkRef = useRef(props.network); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); @@ -90,13 +173,13 @@ function ReportActionsView(props) { if (props.report.isOptimisticReport) { return; } - - Report.openReport(reportID); + Report.openReport({reportID, reportActionID}); }; useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps + // isFirstRender.current = false; }, []); useEffect(() => { @@ -129,7 +212,7 @@ function ReportActionsView(props) { // update ref with current state prevIsSmallScreenWidthRef.current = props.isSmallScreenWidth; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isSmallScreenWidth, props.report, props.reportActions, isReportFullyVisible]); + }, [props.isSmallScreenWidth, props.report, reportActions, isReportFullyVisible]); useEffect(() => { // Ensures subscription event succeeds when the report/workspace room is created optimistically. @@ -153,7 +236,7 @@ function ReportActionsView(props) { return; } - const oldestReportAction = _.last(props.reportActions); + const oldestReportAction = _.last(reportActions); // Don't load more chats if we're already at the beginning of the chat history if (oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { @@ -161,6 +244,7 @@ function ReportActionsView(props) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); + setShowAllReports(true); }; /** @@ -182,11 +266,15 @@ function ReportActionsView(props) { // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFetchNewerWasCalled.current = true; + if (isReportActionArrayCatted || reportActionID) { + setFetchNewerWasCalled(true); return; } - const newestReportAction = _.first(props.reportActions); + if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + return; + } + + const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); }, 700); @@ -211,7 +299,7 @@ function ReportActionsView(props) { }; // Comments have not loaded at all yet do nothing - if (!_.size(props.reportActions)) { + if (!_.size(reportActions)) { return null; } @@ -220,7 +308,7 @@ function ReportActionsView(props) { Date: Tue, 3 Oct 2023 15:25:21 +0200 Subject: [PATCH 005/124] WIP linking --- src/libs/ReportActionsUtils.js | 3 +- src/pages/home/report/ReportActionsList.js | 10 +++++- src/pages/home/report/ReportActionsView.js | 37 ++++++---------------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index cc9efcf6c9cb..4665575f263f 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -355,7 +355,8 @@ function getSlicedRangeFromArrayByID(array, id) { } // return array.slice(0, index+1); - return array.slice(index, array.length); + // return array.slice(index, array.length); + return array.slice(index, array.length - index > 50 ? index + 50 : array.length); } /** diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 31e48dc0e46b..38f3ab5e6763 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -68,6 +68,7 @@ const defaultProps = { isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, + isLinkingLoader: false, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -117,6 +118,7 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, + isLinkingLoader, }) { const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); @@ -328,7 +330,7 @@ function ReportActionsList({ const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; const contentContainerStyle = useMemo( - () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], + () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], [isLoadingNewerReportActions], ); @@ -368,6 +370,12 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions]); + useEffect(() => { + if (!isLinkingLoader) { + return; + } + reportScrollManager.scrollToBottom(); + }, [isLinkingLoader, reportScrollManager]); return ( <> { - console.log('get.ROUTE_CHANGED.triggered', route); if (!reportActionID) { return; } - testRef.current = true; + setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); props.fetchReportIfNeeded(); - console.log('get.ROUTE_CHANGED.+++++++++'); setTimeout(() => { - testRef.current = false; setLinkingToMessageTrigger(false); - console.log('get.ROUTE_CHANGED.+++++++++FINISH', route); - }, 7000); + }, 700); // setLinkingToMessageTrigger(true); }, [route, props.fetchReportIfNeeded, reportActionID]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { - console.log('get.isReportActionArrayCatted.+++++++++'); return true; } - - console.log('get.isReportActionArrayCatted.----------'); return false; - }, [reportActions, allReportActions, reportActionID, isFetchNewerWasCalled]); + }, [reportActions, allReportActions, reportActionID]); const hasCachedActions = useRef(_.size(reportActions) > 0); @@ -244,7 +229,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - setShowAllReports(true); }; /** @@ -312,6 +296,8 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} + isLinkingLoader={!!reportActionID && props.isLoadingInitialReportActions} + isReportActionArrayCatted={isReportActionArrayCatted} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} @@ -351,12 +337,9 @@ function arePropsEqual(oldProps, newProps) { return false; } - // if (oldProps.isLinkingToMessage !== newProps.isLinkingToMessage) { - // return false; - // } - // if (oldisReportActionArrayCatted !== newisReportActionArrayCatted) { - // return false; - // } + if (oldProps.isLoadingNewerReportActions !== newProps.isLoadingNewerReportActions) { + return false; + } if (oldProps.report.lastReadTime !== newProps.report.lastReadTime) { return false; From 241a2994fd542a2ea3c930931465c313b042f70b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 5 Oct 2023 16:26:13 +0200 Subject: [PATCH 006/124] still WIP --- src/pages/home/ReportScreen.js | 72 +++------------------- src/pages/home/report/ReportActionsView.js | 30 +++------ 2 files changed, 15 insertions(+), 87 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a01f95e4f4fa..ddccb6d5fafe 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -149,7 +149,6 @@ function ReportScreen({ route, report, reportMetadata, - // reportActions, sortedReportActions, accountManagerReportID, personalDetails, @@ -176,46 +175,8 @@ function ReportScreen({ const reportActions = useMemo(() => { const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - console.log('get.ROOT.reportActions', reportActionID, sortedReportActions.length, val.length); return val; }, [sortedReportActions, reportActionID]); - // const isReportActionArrayCatted = useMemo(() => sortedReportActions.length !== withoutGaps.length, [withoutGaps, sortedReportActions]); - - // const reportActions = useMemo(() => { - // //TODO: OpenReport, it means that we clicked on the link in current chat and we need to get a proper range of reportActions - // // console.log( - // // 'get.reportActions.initialSorted', - // // // sortedReportActions.length, - // // sortedReportActions.length, - // // !!reportActionID, - // // sortedReportActions.map((item) => { - // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; - // // }), - // // ); - // // // // console.log('get.reportActions.initialSorted', sortedReportActions.map((item) =>item?.previousReportActionID)); - // // console.log( - // // 'get.reportActions.withoutGaps', - // // // withoutGaps.length, - // // withoutGaps.length, - // // !!reportActionID, - // // withoutGaps.map((item) => { - // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; - // // }), - // // ); - // return withoutGaps; - // // return sortedReportActions; - // }, [sortedReportActions, reportActionID]); - - // const reportActionsBeforeAndIncludingLinked = useMemo(() => { - // if (reportActionID) { - // firstRenderRefI.current = false; - // return ReportActionsUtils.getSlicedRangeFromArrayByID(reportActions, reportActionID); - // } - // return null; - // }, [reportActions, reportActionID]); - - // const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); - // >>>>>>> Stashed changes const [isBannerVisible, setIsBannerVisible] = useState(true); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); @@ -285,7 +246,9 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); - // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''})) + const fetchReport = useCallback(() => { + Report.openReport({reportID, reportActionID: reportActionID || ''}); + }, [reportID, reportActionID]); const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); @@ -299,28 +262,12 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - - // useEffect(() => { - // console.log('get.ROUTE.0', route); - // const {reportActionID} = getReportActionID(route); - // if (!reportActionID) return; - // fetchReportIfNeeded(); - // console.log('get.ROUTE.+++++++++', route); - // setLinkingToMessageTrigger(true); - // }, [route, fetchReportIfNeeded]); - // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => { - // console.log('getChat.OPEN_REPORT.000', reportActionID); - // fetchReportIfNeeded(); - // // updateCurrentReportID(getReportID(route)); - // }); - - // if (report.reportID && report.reportID === getReportID(route) && !reportActionID) { if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { return; } - Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''}); - }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions]); + fetchReport(); + }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions, fetchReport]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -432,12 +379,7 @@ function ReportScreen({ // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo( - () => (!firstRenderRef.current && - !report.reportID && - !isOptimisticDelete && - !reportMetadata.isLoadingInitialReportActions && - !isLoading && - !userLeavingStatus) || shouldHideReport, + () => (!firstRenderRef.current && !report.reportID && !isOptimisticDelete && !reportMetadata.isLoadingInitialReportActions && !isLoading && !userLeavingStatus) || shouldHideReport, [report, reportMetadata, isLoading, shouldHideReport, isOptimisticDelete, userLeavingStatus], ); @@ -496,7 +438,7 @@ function ReportScreen({ report={report} isLinkingToMessage={isLinkingToMessage} setLinkingToMessageTrigger={setLinkingToMessageTrigger} - fetchReportIfNeeded={fetchReportIfNeeded} + fetchReport={fetchReport} reportActionID={reportActionID} isLoadingInitialReportActions={reportMetadata.isLoadingInitialReportActions} isLoadingNewerReportActions={reportMetadata.isLoadingNewerReportActions} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3cfda35cd66c..c3ca835d7812 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -20,7 +20,6 @@ import reportPropTypes from '../../reportPropTypes'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import getIsReportFullyVisible from '../../../libs/getIsReportFullyVisible'; import {ReactionListContext} from '../ReportScreenContext'; -import useReportScrollManager from '../../../hooks/useReportScrollManager'; const propTypes = { /** The report currently being looked at */ @@ -83,7 +82,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const {reportActionID} = getReportActionID(route); - const testRef = useRef(false); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); @@ -97,22 +95,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { }, [allReportActions, reportActionID]); const reportActions = useMemo(() => { - console.log( - 'get.reportActions.info|||', - '| reportActionID:', - reportActionID, - '| isFetchNewerWasCalled:', - isFetchNewerWasCalled, - '| isLinkingToMessage:', - isLinkingToMessage, - '| testRef.current:', - testRef.current, - '| reportActionsBeforeAndIncludingLinked:', - reportActionsBeforeAndIncludingLinked?.length, - '| allReportActions:', - allReportActions?.length, - ); - if (!reportActionID || (!isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { return allReportActions; } @@ -124,13 +106,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { return; } setFetchNewerWasCalled(false); + setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); - props.fetchReportIfNeeded(); + props.fetchReport(); setTimeout(() => { setLinkingToMessageTrigger(false); - }, 700); + }, 300); // setLinkingToMessageTrigger(true); - }, [route, props.fetchReportIfNeeded, reportActionID]); + }, [route, reportActionID]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -250,7 +233,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (isReportActionArrayCatted || reportActionID) { + if (!isFetchNewerWasCalled) { setFetchNewerWasCalled(true); return; } @@ -260,6 +243,9 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); + // Report.getNewerActions(reportID, '2420805078232802130'); + // Report.getNewerActions(reportID, '569204055949619736'); + // Report.getNewerActions(reportID, '1134531619271003224'); }, 500); /** From 7ca764369daa8cb53154eb0b9d4d7be3250ea92d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 16:25:44 +0200 Subject: [PATCH 007/124] remove timer --- src/pages/home/report/ReportActionsView.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8c0f5eb77c57..046c062d70be 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -108,9 +108,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); props.fetchReport(); - setTimeout(() => { + const timeoutId = setTimeout(() => { setLinkingToMessageTrigger(false); - }, 300); + }, 500); + + // Return a cleanup function + return () => { + clearTimeout(timeoutId); + }; // setLinkingToMessageTrigger(true); }, [route, reportActionID]); @@ -233,8 +238,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // This should be removed once the issue of frequent re-renders is resolved. if (!isFetchNewerWasCalled) { setFetchNewerWasCalled(true); - // if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // isFetchNewerWasCalled.current = true; return; } if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { From 8ce9ca0e65027ae31cc172d2150b314c6bd90ff0 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 17:55:07 +0200 Subject: [PATCH 008/124] scroll to --- src/pages/home/ReportScreen.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index df85fcca52f0..4a29a05421fc 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -281,11 +281,17 @@ function ReportScreen({ * @param {String} text */ const onSubmitComment = useCallback( - (text) => { - Report.addComment(getReportID(route), text); - }, - [route], - ); + (text) => { + Report.addComment(getReportID(route), text); + // we need to scroll to the bottom of the list after the comment was added + const refID = setTimeout(() => { + flatListRef.current.scrollToOffset({animated: false, offset: 0}); + }, 10); + + return () => clearTimeout(refID); + }, + [route], + ); useFocusEffect( useCallback(() => { From 8efb8fd896b8d6347b334945acea3afa9bba22e1 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 12:00:15 +0200 Subject: [PATCH 009/124] fix getSlicedRangeFromArrayByID --- src/libs/ReportActionsUtils.js | 58 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 373732172992..3d13394c133c 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -311,6 +311,14 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // const updatedActions = processReportActions(reportActions); // console.log(updatedActions); +/** + * Returns the range of report actions from the given array which include current id + * the range is consistent + * + * @param {Array} array + * @param {String} id + * @returns {Array} + */ function getRangeFromArrayByID(array, id) { // without gaps let index; @@ -342,23 +350,45 @@ function getRangeFromArrayByID(array, id) { // return array.slice(startIndex, endIndex); } -function getSlicedRangeFromArrayByID(array, id) { - let index; - if (id) { - index = array.findIndex((obj) => obj.reportActionID === id); - } else { - index = array.length - 1; - } - - if (index === -1) { - return []; - } - // return array.slice(0, index+1); - // return array.slice(index, array.length); - return array.slice(index, array.length - index > 50 ? index + 50 : array.length); +/** + * Returns the sliced range of report actions from the given array. + * + * @param {Array} array + * @param {String} id + * @returns {Object} + * getSlicedRangeFromArrayByID([{id:1}, ..., {id: 100}], 50) => { catted: [{id:1}, ..., {id: 50}], expanded: [{id: 45}, ..., {id: 55}] } + */ +function getSlicedRangeFromArrayByID(array, id) { + let index; + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = array.length - 1; + } + + if (index === -1) { + return { catted: [], expanded: [] }; + } + + const cattedEnd = array.length - index > 50 ? index + 50 : array.length; + const expandedStart = Math.max(0, index - 5); + + const catted = []; + const expanded = []; + + for (let i = expandedStart; i < cattedEnd; i++) { + if (i >= index) { + catted.push(array[i]); + } + expanded.push(array[i]); + } + // We need the expanded version to prevent jittering of list. So when user navigate to linked message we show to him the catted version. After that we show the expanded version. + // Then we can show all reports. + return { catted, expanded }; } + /** * Finds most recent IOU request action ID. * From cfda4c07ff3830e989ed99ed7a122212b7637106 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 12:39:58 +0200 Subject: [PATCH 010/124] update cutting method --- src/pages/home/report/ReportActionsView.js | 79 ++++++++++++++-------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f8ed94618f70..38174a867b80 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -77,47 +77,74 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -function ReportActionsView({reportActions: allReportActions, ...props}) { +function ReportActionsView({reportActions: allReportActions, fetchReport, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); - const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); + const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); + const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const reportActionsBeforeAndIncludingLinked = useMemo(() => { + useEffect(() => { + let timeoutIdCatted; + let timeoutIdExtended; + if (!isLoadingLinkedMessage) { + timeoutIdCatted = setTimeout(() => { + setLinkingToCattedMessage(false); + }, 100); + timeoutIdExtended = setTimeout(() => { + setLinkingToExtendedMessage(false); + }, 200); + } + return () => { + clearTimeout(timeoutIdCatted); + clearTimeout(timeoutIdExtended); + }; + }, [isLoadingLinkedMessage]); + + const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); } - return []; + // catted means the reportActions before and including the linked message + // expanded means the reportActions before and including the linked message plus the next 5 + return {catted: [], expanded: []}; }, [allReportActions, reportActionID]); const reportActions = useMemo(() => { - if (!reportActionID || (!isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { + if (!reportActionID || (!isLinkingToCattedMessage && !isLoadingLinkedMessage && !isLinkingToExtendedMessage)) { return allReportActions; } + if ( + reportActionID && + !isLinkingToCattedMessage && + isLinkingToExtendedMessage && + reportActionsBeforeAndIncludingLinkedExpanded.length !== reportActionsBeforeAndIncludingLinked.length + ) { + return reportActionsBeforeAndIncludingLinkedExpanded; + } return reportActionsBeforeAndIncludingLinked; - }, [isFetchNewerWasCalled, allReportActions, reportActionsBeforeAndIncludingLinked, reportActionID, isLinkingToMessage, props.isLoadingInitialReportActions]); + }, [ + allReportActions, + reportActionsBeforeAndIncludingLinked, + reportActionID, + isLinkingToCattedMessage, + isLoadingLinkedMessage, + isLinkingToExtendedMessage, + reportActionsBeforeAndIncludingLinkedExpanded, + ]); useEffect(() => { if (!reportActionID) { return; } - setFetchNewerWasCalled(false); - setLinkingToMessageTrigger(true); - props.fetchReport(); - const timeoutId = setTimeout(() => { - setLinkingToMessageTrigger(false); - }, 500); - - // Return a cleanup function - return () => { - clearTimeout(timeoutId); - }; - // setLinkingToMessageTrigger(true); - }, [route, reportActionID]); + setLinkingToCattedMessage(true); + setLinkingToExtendedMessage(true); + fetchReport(); + }, [route, reportActionID, fetchReport]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -237,26 +264,18 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (!isFetchNewerWasCalled) { - setFetchNewerWasCalled(true); - return; - } - if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + if (!isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { return; } const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); - // if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // isFetchNewerWasCalled.current = true; - // return; - // } // const newestReportAction = _.first(props.reportActions); // Report.getNewerActions(reportID, newestReportAction.reportActionID); // Report.getNewerActions(reportID, '1134531619271003224'); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions], ); /** From 16c4231733589e88d66e0d98606aacdfe971f8e3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 11 Oct 2023 18:16:50 +0200 Subject: [PATCH 011/124] remove comments --- src/libs/ReportActionsUtils.js | 75 +--------------------- src/libs/ReportUtils.js | 24 ------- src/pages/home/report/ReportActionsView.js | 13 +--- 3 files changed, 4 insertions(+), 108 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 3d13394c133c..363ea4567f40 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -206,45 +206,14 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // * @param {Object} reportActions // * @returns {Array} // */ -// function processReportActions(reportActions) { -// // TODO: +// function processReportActions(reportActions) { //TODO: remove after previousReportActionID is stable // // Separate new and sorted reportActions // const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); // const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); -// // console.log( -// // 'getChat.Sort.N.0', -// // newReportActions.length, -// // newReportActions.map(({message}) => message[0].text), -// // ); -// // console.log( -// // 'getChat.Sort.N.0.0', -// // newReportActions.length, -// // newReportActions.map(({previousReportActionID}) => previousReportActionID), -// // ); -// // console.log( -// // 'getChat.Sort.S.0', -// // sortedReportActions.length, -// // sortedReportActions.map(({message}) => message[0].text), -// // ); -// // console.log( -// // 'getChat.Sort.S.0.0', -// // sortedReportActions.length, -// // sortedReportActions.map(({previousReportActionID}) => previousReportActionID), -// // ); // // Sort the new reportActions // const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); -// // console.log( -// // 'getChat.SORT.SS', -// // JSON.stringify( -// // sortedNewReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first // const processedReportActions = sortedNewReportActions.map((action, index) => { @@ -257,59 +226,22 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // }; // }); -// // console.log( -// // 'getChat.SORT.BEFORE', -// // JSON.stringify( -// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { // processedReportActions.pop(); // } -// // console.log( -// // 'getChat.SORT.AFTER', -// // JSON.stringify( -// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // // Determine the order of merging based on reportActionID values // const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; // const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; -// // console.log('getChat.Sort.1', getSortedReportActionsForDisplay(reportActions).length, [...sortedReportActions, ...processedReportActions].length); -// // console.log('getChat.Sort.1.1', _.last(sortedReportActions), _.first(processedReportActions)); // if (firstProcessedReportActionID > lastSortedReportActionID) { -// // console.log( -// // 'getChat.Sort.2', -// // [...sortedReportActions, ...processedReportActions].map(({message}) => message[0].text), -// // ); // return [...sortedReportActions, ...processedReportActions]; // } else { -// // console.log( -// // 'getChat.Sort.3', -// // [...processedReportActions, ...sortedReportActions].map(({message}) => message[0].text), -// // ); // return [...processedReportActions, ...sortedReportActions]; // } // } -// Usage: -// const reportActions = [ -// { reportActionID: '1' }, -// { reportActionID: '2', previousReportActionID: '1' }, -// { reportActionID: '3' } -// ]; -// const updatedActions = processReportActions(reportActions); -// console.log(updatedActions); + /** * Returns the range of report actions from the given array which include current id @@ -320,7 +252,6 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal * @returns {Array} */ function getRangeFromArrayByID(array, id) { - // without gaps let index; if (id) { @@ -347,7 +278,6 @@ function getRangeFromArrayByID(array, id) { } return array.slice(startIndex, endIndex + 1); - // return array.slice(startIndex, endIndex); } @@ -874,7 +804,6 @@ export { getAllReportActions, isReportActionAttachment, isNotifiableReportAction, - // processReportActions, getRangeFromArrayByID, getSlicedRangeFromArrayByID, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 79f052bd21b4..4a4876132220 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3807,29 +3807,6 @@ function getIOUReportActionDisplayMessage(reportAction) { return displayMessage; } -/** - * Hook that triggers a fetch when reportActionID appears while reportID stays the same. - * - * @param {string} reportID - The current reportID from the route. - * @param {string|null} reportActionID - The current reportActionID from the route or null. - * @param {function} triggerFetch - The function to be triggered on the condition. - */ -function useRouteChangeHandler(reportID = '', reportActionID = '', triggerFetch = () => {}) { - // Store the previous reportID and reportActionID - const previousReportIDRef = useRef(null); - const previousReportActionIDRef = useRef(null); - - useEffect(() => { - // Check if reportID is the same and reportActionID just appeared - if (reportID === previousReportIDRef.current && reportActionID && !previousReportActionIDRef.current) { - triggerFetch(); - } - - // Update refs for the next render - previousReportIDRef.current = reportID; - previousReportActionIDRef.current = reportActionID; - }, [reportID, reportActionID, triggerFetch]); - } /** * @param {Object} report * @returns {Boolean} @@ -3982,6 +3959,5 @@ export { hasMissingSmartscanFields, getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, - useRouteChangeHandler, isReportDraft, }; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 06e0b4d865fc..eaa4d9bf3a24 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -84,13 +84,11 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - + const isFirstRender = useRef(true); const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const isFirstRender = useRef(true); //TODO: - // const hasCachedActions = useRef(_.size(props.reportActions) > 0); //TODO: useEffect(() => { @@ -183,7 +181,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - // isFirstRender.current = false; }, []); useEffect(() => { @@ -269,21 +266,15 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro // // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - - // if (!isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { // TODO: // // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - if (isFirstRender.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { isFirstRender.current = false; return; } const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); - - // const newestReportAction = _.first(props.reportActions); - // Report.getNewerActions(reportID, newestReportAction.reportActionID); - // Report.getNewerActions(reportID, '1134531619271003224'); }, 500), [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions], ); From adb4162f36d17c3eec13e3cf44d2a77a75769a1a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 24 Oct 2023 13:43:48 +0200 Subject: [PATCH 012/124] include deleted message in getRangeFromArrayByID --- src/libs/ReportActionsUtils.ts | 16 +++++++++++---- src/pages/home/ReportScreen.js | 36 ++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b329281a3078..071505d1067e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -19,8 +19,8 @@ type LastVisibleMessage = { }; type SlicedResult = { - catted: ReportAction[]; - expanded: ReportAction[]; + catted: ReportAction[]; + expanded: ReportAction[]; }; const allReports: OnyxCollection = {}; @@ -218,7 +218,7 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort * param {String} id * returns {ReportAction} */ -function getRangeFromArrayByID(array: ReportAction[], id: string): ReportAction[] { +function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction[] { let index; if (id) { @@ -543,12 +543,19 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): */ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { const filteredReportActions = Object.entries(reportActions ?? {}) - .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) + // .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); } +function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { + if (!reportActions) { + return []; + } + return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); +} + /** * In some cases, there can be multiple closed report actions in a chat report. * This method returns the last closed report action so we can always show the correct archived report reason. @@ -734,6 +741,7 @@ export { getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, + getReportActionsWithoutRemoved, isConsecutiveActionMadeByPreviousActor, isCreatedAction, isCreatedTaskReportAction, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index c1acb0d3de40..0a73be16297c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -150,7 +150,8 @@ function ReportScreen({ route, report, reportMetadata, - sortedReportActions, + // sortedReportActions, + allReportActions, accountManagerReportID, personalDetails, markReadyForHydration, @@ -175,9 +176,12 @@ function ReportScreen({ const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); const reportActions = useMemo(() => { - const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - return val; - }, [sortedReportActions, reportActionID]); + if (allReportActions?.length === 0) return []; + const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sorterReportActions, reportActionID); + const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); + return reportActionsWithoutDeleted; + }, [reportActionID, allReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); @@ -289,10 +293,10 @@ function ReportScreen({ flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, 10); - return () => clearTimeout(refID); - }, - [route], - ); + return () => clearTimeout(refID); + }, + [route], + ); useFocusEffect( useCallback(() => { @@ -455,7 +459,6 @@ function ReportScreen({ onLayout={onListLayout} > {isReportReadyForDisplay && !isLoading && ( - `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, @@ -550,12 +552,12 @@ export default compose( key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, - sortedReportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, - // selector: ReportActionsUtils.processReportActions, - }, + // sortedReportActions: { + // key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + // canEvict: false, + // selector: ReportActionsUtils.getSortedReportActionsForDisplay, + // // selector: ReportActionsUtils.processReportActions, + // }, }, true, ), From 1f073b03dc0192086ab3c215dd535a271dfb1e85 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 7 Nov 2023 19:59:23 +0100 Subject: [PATCH 013/124] fix sliding --- .../InvertedFlatList/BaseInvertedFlatList.js | 2 +- src/pages/home/report/ReportActionsList.js | 11 +---- src/pages/home/report/ReportActionsView.js | 40 +++++++++++-------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 802ae373d22a..b71311b0a173 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -136,7 +136,7 @@ function BaseInvertedFlatList(props) { windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, - autoscrollToTopThreshold: variables.listItemHeightNormal, + // autoscrollToTopThreshold: variables.listItemHeightNormal, }} inverted /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 5520221c3b56..b0e62281845b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -130,9 +130,8 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, - isLinkingLoader, + reportScrollManager, }) { - const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const route = useRoute(); @@ -360,7 +359,7 @@ function ReportActionsList({ const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; const contentContainerStyle = useMemo( - () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], + () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], [isLoadingNewerReportActions], ); @@ -404,12 +403,6 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions]); - useEffect(() => { - if (!isLinkingLoader) { - return; - } - reportScrollManager.scrollToBottom(); - }, [isLinkingLoader, reportScrollManager]); return ( <> { - let timeoutIdCatted; - let timeoutIdExtended; - if (!isLoadingLinkedMessage) { - timeoutIdCatted = setTimeout(() => { - setLinkingToCattedMessage(false); - }, 100); - timeoutIdExtended = setTimeout(() => { - setLinkingToExtendedMessage(false); - }, 200); - } - return () => { - clearTimeout(timeoutIdCatted); - clearTimeout(timeoutIdExtended); - }; - }, [isLoadingLinkedMessage]); const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { @@ -158,10 +145,28 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if (!reportActionID) { return; } + if (scrollToBottom) { + scrollToBottom(); + } setLinkingToCattedMessage(true); setLinkingToExtendedMessage(true); fetchReport(); - }, [route, reportActionID, fetchReport]); + + const timeoutIdCatted = setTimeout(() => { + setLinkingToCattedMessage(false); + }, 100); + const timeoutIdExtended = setTimeout(() => { + setLinkingToExtendedMessage(false); + }, 200); + + return () => { + if (!timeoutIdCatted && !timeoutIdExtended) { + return; + } + clearTimeout(timeoutIdCatted); + clearTimeout(timeoutIdExtended); + }; + }, [route, reportActionID, fetchReport, scrollToBottom]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -346,6 +351,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} + reportScrollManager={reportScrollManager} policy={props.policy} /> From 2b7a7355d25c7207cccd723b5322ef90884777b6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 12 Nov 2023 18:06:39 +0100 Subject: [PATCH 014/124] fix getSlicedRangeFromArrayByID --- src/libs/ReportActionsUtils.ts | 17 +++------ src/pages/home/report/ReportActionsView.js | 42 +++++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b927febb8c13..a1a236201d5b 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -259,7 +259,7 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction * returns {Object} * getSlicedRangeFromArrayByID([{id:1}, ..., {id: 100}], 50) => { catted: [{id:1}, ..., {id: 50}], expanded: [{id: 45}, ..., {id: 55}] } */ -function getSlicedRangeFromArrayByID(array: ReportAction[], id: string):SlicedResult { +function getSlicedRangeFromArrayByID(array: ReportAction[], id: string): SlicedResult { let index; if (id) { index = array.findIndex((obj) => obj.reportActionID === id); @@ -271,18 +271,11 @@ function getSlicedRangeFromArrayByID(array: ReportAction[], id: string):SlicedRe return {catted: [], expanded: []}; } - const cattedEnd = array.length - index > 50 ? index + 50 : array.length; - const expandedStart = Math.max(0, index - 5); + const amountOfItemsBeforeLinkedOne = 25; + const expandedStart = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - const catted: ReportAction[] = []; - const expanded: ReportAction[] = []; - - for (let i = expandedStart; i < cattedEnd; i++) { - if (i >= index) { - catted.push(array[i]); - } - expanded.push(array[i]); - } + const catted: ReportAction[] = array.slice(index, array.length); + const expanded: ReportAction[] = array.slice(expandedStart, array.length); // We need the expanded version to prevent jittering of list. So when user navigate to linked message we show to him the catted version. After that we show the expanded version. // Then we can show all reports. return {catted, expanded}; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 6ee9ae789b8c..fc3a341a3935 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -103,12 +103,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); + const timeoutIdCatted = useRef(null); + const timeoutIdExtended = useRef(null); const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); @@ -125,8 +126,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if ( reportActionID && !isLinkingToCattedMessage && - isLinkingToExtendedMessage && - reportActionsBeforeAndIncludingLinkedExpanded.length !== reportActionsBeforeAndIncludingLinked.length + isLinkingToExtendedMessage ) { return reportActionsBeforeAndIncludingLinkedExpanded; } @@ -142,30 +142,32 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro ]); useEffect(() => { - if (!reportActionID) { + if (isLoadingLinkedMessage) { return; } if (scrollToBottom) { scrollToBottom(); } - setLinkingToCattedMessage(true); - setLinkingToExtendedMessage(true); - fetchReport(); - const timeoutIdCatted = setTimeout(() => { + timeoutIdCatted.current = setTimeout(() => { setLinkingToCattedMessage(false); }, 100); - const timeoutIdExtended = setTimeout(() => { + timeoutIdExtended.current = setTimeout(() => { setLinkingToExtendedMessage(false); }, 200); + }, [isLoadingLinkedMessage, scrollToBottom]); + + useEffect(() => { + if (!reportActionID) { + return; + } + if (scrollToBottom) { + scrollToBottom(); + } + setLinkingToCattedMessage(true); + setLinkingToExtendedMessage(true); + fetchReport(); - return () => { - if (!timeoutIdCatted && !timeoutIdExtended) { - return; - } - clearTimeout(timeoutIdCatted); - clearTimeout(timeoutIdExtended); - }; }, [route, reportActionID, fetchReport, scrollToBottom]); const isReportActionArrayCatted = useMemo(() => { @@ -202,6 +204,14 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps + + return () => { + if (!timeoutIdCatted && !timeoutIdExtended) { + return; + } + clearTimeout(timeoutIdCatted.current); + clearTimeout(timeoutIdExtended.current); + }; }, []); useEffect(() => { From 9f8621852748158ed52ddef21dec575a31b33312 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 09:55:12 +0100 Subject: [PATCH 015/124] lint --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 1 - src/pages/home/ReportScreen.js | 6 +++--- src/pages/home/report/ReportActionsView.js | 7 +------ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 18dc09b9feb1..2862236daa07 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -17,7 +17,6 @@ const defaultProps = { data: [], }; - const BaseInvertedFlatList = forwardRef((props, ref) => ( { From 2d083d329e4f5e0b2677bff3dc55d7b0dd9497e5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 18 Dec 2023 13:16:23 +0100 Subject: [PATCH 016/124] WIP pagination --- src/pages/home/report/ReportActionsView.js | 290 ++++++++++++++------- 1 file changed, 192 insertions(+), 98 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3abb59329b14..fccc6c8d7a66 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,7 +1,10 @@ +/* eslint-disable no-else-return */ + +/* eslint-disable rulesdir/prefer-underscore-method */ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -93,6 +96,95 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } +const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage, cb) => { + const [edgeID, setEdgeID] = useState(linkedID); + const prevLinkedID = useRef(linkedID); + const test = useRef(true); + // Function to calculate the current slice of the message array + + // const listID = useMemo(() => { + // // return reportActionID || 'list' + // console.log('route.key', route); + // return route.key + Math.random().toString; + // }, [route]); + + const index = useMemo(() => { + if (!linkedID) { + return -1; + } + + return messageArray.findIndex((obj) => String(obj.reportActionID) === String(edgeID || linkedID)); + }, [messageArray, linkedID, edgeID, isLoadingLinkedMessage]); + + useEffect(() => { + console.log('get.useHandleList.setEdgeID_EMPTY', linkedID !== prevLinkedID.current, linkedID, prevLinkedID.current); + setEdgeID(''); + // if (linkedID !== prevLinkedID.current) { + // setEdgeID(''); + // prevLinkedID.current = linkedID; + // } + test.current = false; + }, [route, linkedID]); + + console.log('get.useHandleList.INFO.index', index); + console.log('get.useHandleList.INFO.linkedID', linkedID); + console.log('get.useHandleList.INFO.messageArray', messageArray.length); + + const cattedArray = useMemo(() => { + if (!linkedID) { + return messageArray; + } + + if (index === -1) { + return messageArray; + } + console.log('get.useHandleList.calculateSlice.0.index', index); + if (linkedID && !edgeID) { + console.log('get.useHandleList.calculateSlice.1.linkedID', linkedID); + cb(); + return messageArray.slice(index, messageArray.length); + } else if (linkedID && edgeID) { + console.log('get.useHandleList.calculateSlice.2.linkedID_edgeID', linkedID, edgeID); + const amountOfItemsBeforeLinkedOne = 49; + const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; + console.log('get.useHandleList.calculateSlice.2.index_newStartIndex', index, newStartIndex); + if (index) { + return messageArray.slice(newStartIndex, messageArray.length); + } + return messageArray; + } + return messageArray; + }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); + + // const cattedArray = calculateSlice(); + + const hasMoreCashed = cattedArray.length < messageArray.length; + + // Function to handle pagination (dummy in this case, as actual slicing is done in calculateSlice) + const paginate = useCallback( + ({firstReportActionID, distanceFromStart}) => { + // This function is a placeholder as the actual pagination is handled by calculateSlice + // It's here if you need to trigger any side effects during pagination + if (!hasMoreCashed) { + console.log('get.useHandleList.paginate.0.NO_CACHE'); + fetchFn({distanceFromStart}); + } + console.log('get.useHandleList.paginate.1.firstReportActionID'); + setEdgeID(firstReportActionID); + }, + [setEdgeID, fetchFn, hasMoreCashed], + ); + + return { + cattedArray, + fetchFunc: paginate, + linkedIdIndex: index, + setNull: () => { + setEdgeID(''); + }, + }; +}; + function ReportActionsView({reportActions: allReportActions, fetchReport, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); @@ -102,76 +194,88 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const isFirstRender = useRef(true); - const timeoutIdCatted = useRef(null); - const timeoutIdExtended = useRef(null); + // const isFirstRender = useRef(true); - const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); - const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); + const [listID, setListID] = useState('1'); + const [isLinkingLoading, setLinkingLoading] = useState(!!reportActionID); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { - if (reportActionID && allReportActions?.length) { - return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); - } - // catted means the reportActions before and including the linked message - // expanded means the reportActions before and including the linked message plus the next 5 - return {catted: [], expanded: []}; - }, [allReportActions, reportActionID]); - - const reportActions = useMemo(() => { - if (!reportActionID || (!isLinkingToCattedMessage && !isLoadingLinkedMessage && !isLinkingToExtendedMessage)) { - return allReportActions; - } - if (reportActionID && !isLinkingToCattedMessage && isLinkingToExtendedMessage) { - return reportActionsBeforeAndIncludingLinkedExpanded; - } - return reportActionsBeforeAndIncludingLinked; - }, [ - allReportActions, - reportActionsBeforeAndIncludingLinked, + console.log('get.isLoadingLinkedMessage', isLoadingLinkedMessage); + // const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { + // if (reportActionID && allReportActions?.length) { + // return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); + // } + // // catted means the reportActions before and including the linked message + // // expanded means the reportActions before and including the linked message plus the next 5 + // return {catted: [], expanded: []}; + // }, [allReportActions, reportActionID]); + + /** + * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently + * displaying. + */ + const throttledLoadNewerChats = useCallback( + ({distanceFromStart}) => { + console.log('get.throttledLoadNewerChats.0'); + // return null; + if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { + return; + } + console.log('get.throttledLoadNewerChats.1'); + + // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', + // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. + // + // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not + // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. + // + // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. + // This should be removed once the issue of frequent re-renders is resolved. + // + // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call + // if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + // if (isLinkingToExtendedMessage) { + // console.log('get.throttledLoadNewerChats.2', isFirstRender.current, isLinkingToExtendedMessage, distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT, distanceFromStart); + // // isFirstRender.current = false; + // return; + // } + + console.log('get.throttledLoadNewerChats.3'); + const newestReportAction = reportActions[0]; + Report.getNewerActions(reportID, newestReportAction.reportActionID); + }, + // [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions, hasNewestReportAction], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions], + ); + const triggerList = useCallback(() => { + setListID(id => id + 1) + },[setListID]) + + const { + cattedArray: reportActions, + fetchFunc, + linkedIdIndex, + setNull, + } = useHandleList( reportActionID, - isLinkingToCattedMessage, + allReportActions, + throttledLoadNewerChats, + route, isLoadingLinkedMessage, - isLinkingToExtendedMessage, - reportActionsBeforeAndIncludingLinkedExpanded, - ]); - - useEffect(() => { - if (isLoadingLinkedMessage) { - return; - } - if (scrollToBottom) { - scrollToBottom(); - } - - timeoutIdCatted.current = setTimeout(() => { - setLinkingToCattedMessage(false); - }, 100); - timeoutIdExtended.current = setTimeout(() => { - setLinkingToExtendedMessage(false); - }, 200); - }, [isLoadingLinkedMessage, scrollToBottom]); + triggerList + ); useEffect(() => { + console.log('get.useEffect.reportActionID', reportActionID); if (!reportActionID) { return; } - if (scrollToBottom) { - scrollToBottom(); - } - setLinkingToCattedMessage(true); - setLinkingToExtendedMessage(true); + console.log('get.useEffect.route', JSON.stringify(route)); fetchReport(); + // setNull() + // setListID(`${route.key}_${reportActionID}` ) }, [route, reportActionID, fetchReport, scrollToBottom]); - const isReportActionArrayCatted = useMemo(() => { - if (reportActions?.length !== allReportActions?.length && reportActionID) { - return true; - } - return false; - }, [reportActions, allReportActions, reportActionID]); - const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); @@ -184,6 +288,25 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const reportID = props.report.reportID; const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); + // const listID = useMemo( + // () => + // // return reportActionID || 'list'; + // // console.log('route.key', route); + // `${route.key}_${reportActionID}`, + // // `${route.key}`, + // [reportActionID, route, isLinkingLoading], + // ); + // useEffect(() => { + // scrollToBottom(); + // }, [listID, scrollToBottom]); + + useEffect(() => { + if (!reportActionID) { + return; + } + setLinkingLoading(!!reportActionID); + }, [route, reportActionID]); + /** * @returns {Boolean} */ @@ -201,14 +324,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - - return () => { - if (!timeoutIdCatted && !timeoutIdExtended) { - return; - } - clearTimeout(timeoutIdCatted.current); - clearTimeout(timeoutIdExtended.current); - }; }, []); useEffect(() => { @@ -287,36 +402,15 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro Report.getOlderActions(reportID, oldestReportAction.reportActionID); }; - /** - * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently - * displaying. - */ - const loadNewerChats = useMemo( - () => - _.throttle(({distanceFromStart}) => { - if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions || hasNewestReportAction) { - return; - } - - // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', - // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. - // - // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not - // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. - // - // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. - // This should be removed once the issue of frequent re-renders is resolved. - // - // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFirstRender.current = false; - return; - } - - const newestReportAction = reportActions[0]; - Report.getNewerActions(reportID, newestReportAction.reportActionID); - }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions, hasNewestReportAction], + const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); + const handleLoadNewerChats = useCallback( + ({distanceFromStart}) => { + if ((reportActionID && linkedIdIndex > -1) || (!reportActionID && !hasNewestReportAction)) { + setLinkingLoading(false); + fetchFunc({firstReportActionID, distanceFromStart}); + } + }, + [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID], ); /** @@ -352,14 +446,14 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro sortedReportActions={reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} - loadNewerChats={loadNewerChats} + loadNewerChats={handleLoadNewerChats} isLinkingLoader={!!reportActionID && props.isLoadingInitialReportActions} - isReportActionArrayCatted={isReportActionArrayCatted} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} reportScrollManager={reportScrollManager} policy={props.policy} + listID={listID} /> From 37b41c17263850a1367ed9bdd4111b3129cb7d45 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:04:47 +0100 Subject: [PATCH 017/124] fix linking --- src/pages/home/report/ReportActionsView.js | 166 ++++----------------- 1 file changed, 27 insertions(+), 139 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index fccc6c8d7a66..41bdff7a74a3 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -96,17 +96,9 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage, cb) => { +const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { const [edgeID, setEdgeID] = useState(linkedID); - const prevLinkedID = useRef(linkedID); - const test = useRef(true); - // Function to calculate the current slice of the message array - - // const listID = useMemo(() => { - // // return reportActionID || 'list' - // console.log('route.key', route); - // return route.key + Math.random().toString; - // }, [route]); + const [listID, setListID] = useState(1); const index = useMemo(() => { if (!linkedID) { @@ -114,40 +106,23 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe } return messageArray.findIndex((obj) => String(obj.reportActionID) === String(edgeID || linkedID)); - }, [messageArray, linkedID, edgeID, isLoadingLinkedMessage]); + }, [messageArray, linkedID, edgeID]); - useEffect(() => { - console.log('get.useHandleList.setEdgeID_EMPTY', linkedID !== prevLinkedID.current, linkedID, prevLinkedID.current); + useMemo(() => { setEdgeID(''); - // if (linkedID !== prevLinkedID.current) { - // setEdgeID(''); - // prevLinkedID.current = linkedID; - // } - test.current = false; }, [route, linkedID]); - console.log('get.useHandleList.INFO.index', index); - console.log('get.useHandleList.INFO.linkedID', linkedID); - console.log('get.useHandleList.INFO.messageArray', messageArray.length); - const cattedArray = useMemo(() => { - if (!linkedID) { + if (!linkedID || index === -1) { return messageArray; } - if (index === -1) { - return messageArray; - } - console.log('get.useHandleList.calculateSlice.0.index', index); if (linkedID && !edgeID) { - console.log('get.useHandleList.calculateSlice.1.linkedID', linkedID); - cb(); + setListID((i) => i + 1); return messageArray.slice(index, messageArray.length); } else if (linkedID && edgeID) { - console.log('get.useHandleList.calculateSlice.2.linkedID_edgeID', linkedID, edgeID); - const amountOfItemsBeforeLinkedOne = 49; + const amountOfItemsBeforeLinkedOne = 10; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - console.log('get.useHandleList.calculateSlice.2.index_newStartIndex', index, newStartIndex); if (index) { return messageArray.slice(newStartIndex, messageArray.length); } @@ -156,20 +131,16 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe return messageArray; }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); - // const cattedArray = calculateSlice(); - const hasMoreCashed = cattedArray.length < messageArray.length; - // Function to handle pagination (dummy in this case, as actual slicing is done in calculateSlice) const paginate = useCallback( ({firstReportActionID, distanceFromStart}) => { - // This function is a placeholder as the actual pagination is handled by calculateSlice + // This function is a placeholder as the actual pagination is handled by cattedArray // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { - console.log('get.useHandleList.paginate.0.NO_CACHE'); fetchFn({distanceFromStart}); } - console.log('get.useHandleList.paginate.1.firstReportActionID'); + setEdgeID(firstReportActionID); }, [setEdgeID, fetchFn, hasMoreCashed], @@ -179,9 +150,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe cattedArray, fetchFunc: paginate, linkedIdIndex: index, - setNull: () => { - setEdgeID(''); - }, + listID, }; }; @@ -189,123 +158,42 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const reportScrollManager = useReportScrollManager(); - const {scrollToBottom} = reportScrollManager; const route = useRoute(); const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - // const isFirstRender = useRef(true); - - const [listID, setListID] = useState('1'); - const [isLinkingLoading, setLinkingLoading] = useState(!!reportActionID); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; + const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); + const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + + const prevNetworkRef = useRef(props.network); + const prevAuthTokenType = usePrevious(props.session.authTokenType); - console.log('get.isLoadingLinkedMessage', isLoadingLinkedMessage); - // const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { - // if (reportActionID && allReportActions?.length) { - // return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); - // } - // // catted means the reportActions before and including the linked message - // // expanded means the reportActions before and including the linked message plus the next 5 - // return {catted: [], expanded: []}; - // }, [allReportActions, reportActionID]); + const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); + const isFocused = useIsFocused(); + const reportID = props.report.reportID; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ const throttledLoadNewerChats = useCallback( - ({distanceFromStart}) => { - console.log('get.throttledLoadNewerChats.0'); - // return null; + () => { if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { return; } - console.log('get.throttledLoadNewerChats.1'); - - // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', - // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. - // - // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not - // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. - // - // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. - // This should be removed once the issue of frequent re-renders is resolved. - // - // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - // if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // if (isLinkingToExtendedMessage) { - // console.log('get.throttledLoadNewerChats.2', isFirstRender.current, isLinkingToExtendedMessage, distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT, distanceFromStart); - // // isFirstRender.current = false; - // return; - // } - - console.log('get.throttledLoadNewerChats.3'); - const newestReportAction = reportActions[0]; + + // eslint-disable-next-line no-use-before-define Report.getNewerActions(reportID, newestReportAction.reportActionID); }, - // [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions, hasNewestReportAction], - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions], + // eslint-disable-next-line no-use-before-define + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], ); - const triggerList = useCallback(() => { - setListID(id => id + 1) - },[setListID]) - - const { - cattedArray: reportActions, - fetchFunc, - linkedIdIndex, - setNull, - } = useHandleList( - reportActionID, - allReportActions, - throttledLoadNewerChats, - route, - isLoadingLinkedMessage, - triggerList - ); - - useEffect(() => { - console.log('get.useEffect.reportActionID', reportActionID); - if (!reportActionID) { - return; - } - console.log('get.useEffect.route', JSON.stringify(route)); - fetchReport(); - // setNull() - // setListID(`${route.key}_${reportActionID}` ) - }, [route, reportActionID, fetchReport, scrollToBottom]); - const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); - const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - - const prevNetworkRef = useRef(props.network); - const prevAuthTokenType = usePrevious(props.session.authTokenType); + const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, isLoadingLinkedMessage); - const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); - - const isFocused = useIsFocused(); - const reportID = props.report.reportID; const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); - - // const listID = useMemo( - // () => - // // return reportActionID || 'list'; - // // console.log('route.key', route); - // `${route.key}_${reportActionID}`, - // // `${route.key}`, - // [reportActionID, route, isLinkingLoading], - // ); - // useEffect(() => { - // scrollToBottom(); - // }, [listID, scrollToBottom]); - - useEffect(() => { - if (!reportActionID) { - return; - } - setLinkingLoading(!!reportActionID); - }, [route, reportActionID]); + const newestReportAction = lodashGet(reportActions, '[0]'); /** * @returns {Boolean} @@ -404,9 +292,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); const handleLoadNewerChats = useCallback( + // eslint-disable-next-line rulesdir/prefer-early-return ({distanceFromStart}) => { - if ((reportActionID && linkedIdIndex > -1) || (!reportActionID && !hasNewestReportAction)) { - setLinkingLoading(false); + if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction) || (!reportActionID && !hasNewestReportAction)) { fetchFunc({firstReportActionID, distanceFromStart}); } }, From a42d360757c0f95e0aaebb066e395f1f0564234e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:05:26 +0100 Subject: [PATCH 018/124] remove autoscrollToTopThreshold --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 2862236daa07..abfad0f04be1 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -2,8 +2,6 @@ import PropTypes from 'prop-types'; import React, {forwardRef} from 'react'; import FlatList from '@components/FlatList'; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; - const propTypes = { /** Same as FlatList can be any array of anything */ // eslint-disable-next-line react/forbid-prop-types @@ -25,7 +23,6 @@ const BaseInvertedFlatList = forwardRef((props, ref) => ( windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, - // autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD, }} inverted /> From 67d530e8b8419b38ac673f546a277cc0b79f15e8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:05:59 +0100 Subject: [PATCH 019/124] clean ReportActionsUtils --- src/libs/ReportActionsUtils.ts | 35 ---------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4959c586a5fa..7eae4ce4d9e4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -23,10 +23,6 @@ type LastVisibleMessage = { lastMessageHtml?: string; }; -type SlicedResult = { - catted: ReportAction[]; - expanded: ReportAction[]; -}; type MemberChangeMessageUserMentionElement = { readonly kind: 'userMention'; readonly accountID: number; @@ -302,36 +298,6 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction return array.slice(startIndex, endIndex + 1); } -/** - * Returns the sliced range of report actions from the given array. - * - * param {ReportAction[]} array - * param {String} id - * returns {Object} - * getSlicedRangeFromArrayByID([{id:1}, ..., {id: 100}], 50) => { catted: [{id:1}, ..., {id: 50}], expanded: [{id: 45}, ..., {id: 55}] } - */ -function getSlicedRangeFromArrayByID(array: ReportAction[], id: string): SlicedResult { - let index; - if (id) { - index = array.findIndex((obj) => obj.reportActionID === id); - } else { - index = array.length - 1; - } - - if (index === -1) { - return {catted: [], expanded: []}; - } - - const amountOfItemsBeforeLinkedOne = 25; - const expandedStart = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - - const catted: ReportAction[] = array.slice(index, array.length); - const expanded: ReportAction[] = array.slice(expandedStart, array.length); - // We need the expanded version to prevent jittering of list. So when user navigate to linked message we show to him the catted version. After that we show the expanded version. - // Then we can show all reports. - return {catted, expanded}; -} - /** * Finds most recent IOU request action ID. */ @@ -936,7 +902,6 @@ export { shouldReportActionBeVisible, shouldReportActionBeVisibleAsLastAction, getRangeFromArrayByID, - getSlicedRangeFromArrayByID, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, isMemberChangeAction, From aaf0377fb3bacf2f825d844d497b483d5fa4a2f8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:10:57 +0100 Subject: [PATCH 020/124] update initialNumToRender for web and desktop --- src/pages/home/report/ReportActionsList.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 36203cdd87a7..ac02c2974560 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -15,6 +15,7 @@ import useReportScrollManager from '@hooks/useReportScrollManager'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; +import getPlatform from '@libs/getPlatform'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; @@ -152,6 +153,8 @@ function ReportActionsList({ } return cacheUnreadMarkers.get(report.reportID); }; + const platform = getPlatform(); + const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID; const [currentUnreadMarker, setCurrentUnreadMarker] = useState(markerInit); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); @@ -314,8 +317,14 @@ function ReportActionsList({ const initialNumToRender = useMemo(() => { const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); - return Math.ceil(availableHeight / minimumReportActionHeight); - }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight]); + const numToRender = Math.ceil(availableHeight / minimumReportActionHeight); + if (linkedReportActionID && !isNative) { + // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. + + return Math.max(numToRender, 50); + } + return numToRender; + }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight, linkedReportActionID, isNative]); /** * Thread's divider line should hide when the first chat in the thread is marked as unread. From d6a9f584b304fca5d08766b25594a74d961a2498 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:11:30 +0100 Subject: [PATCH 021/124] add listID for resetting --- src/pages/home/report/ReportActionsList.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index ac02c2974560..082f1437b5ed 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -139,6 +139,7 @@ function ReportActionsList({ onLayout, isComposerFullSize, reportScrollManager, + listID, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -483,6 +484,7 @@ function ReportActionsList({ onScroll={trackVerticalScrolling} onScrollToIndexFailed={() => {}} extraData={extraData} + key={listID} /> From 45fbbf0b52930ed4684f84a87c60cc56037d23e3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:12:24 +0100 Subject: [PATCH 022/124] WIP: fix MVCPFlatList --- src/components/FlatList/MVCPFlatList.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index c9ec3c6a95c1..b6199e9174dd 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -1,7 +1,7 @@ /* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ import PropTypes from 'prop-types'; import React from 'react'; -import {FlatList} from 'react-native'; +import {FlatList, InteractionManager} from 'react-native'; function mergeRefs(...args) { return function forwardRef(node) { @@ -137,8 +137,10 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); React.useEffect(() => { - prepareForMaintainVisibleContentPosition(); - setupMutationObserver(); + InteractionManager.runAfterInteractions(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }); }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); const setMergedRef = useMergeRefs(scrollRef, forwardedRef); From 736a1e6e0fbbec21709067e39e8d41075d4770e4 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Dec 2023 16:54:45 +0100 Subject: [PATCH 023/124] fix after merge --- src/pages/home/ReportScreen.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 75fb165ec475..6af7b7c2f724 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -154,7 +154,6 @@ function ReportScreen({ route, report, reportMetadata, - // sortedReportActions, allReportActions, accountManagerReportID, personalDetails, @@ -182,11 +181,11 @@ function ReportScreen({ const reportActions = useMemo(() => { if (allReportActions?.length === 0) return []; - const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sorterReportActions, reportActionID); const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); return reportActionsWithoutDeleted; - }, [reportActionID, allReportActions]); + }, [reportActionID, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -199,11 +198,9 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; - // eslint-disable-next-line react-hooks/exhaustive-deps -- need to re-filter the report actions when network status changes - const filteredReportActions = useMemo(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), [isOffline, reportActions]); // 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(filteredReportActions) && reportMetadata.isLoadingInitialReportActions; + const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; @@ -470,7 +467,7 @@ function ReportScreen({ > {isReportReadyForDisplay && !isLoading && ( Date: Wed, 20 Dec 2023 16:55:22 +0100 Subject: [PATCH 024/124] another fix after merge --- src/pages/home/report/ReportActionsView.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 5be82b410b5b..3a6dd00c0699 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -219,6 +219,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (!reportActionID) { + return; + } + Report.openReport({reportID, reportActionID}); + }, [route]); + useEffect(() => { const prevNetwork = prevNetworkRef.current; // When returning from offline to online state we want to trigger a request to OpenReport which From d94d8d6be3a2ddad450674afd6aadf724672afb0 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Dec 2023 09:38:01 +0100 Subject: [PATCH 025/124] add pending reportAction to sorted list --- src/libs/ReportActionsUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9cea9f1100da..b3f46792fff3 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -275,7 +275,7 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction if (id) { index = array.findIndex((obj) => obj.reportActionID === id); } else { - index = 0; + index = array.findIndex((obj) => obj.pendingAction !== 'add'); } if (index === -1) { @@ -285,13 +285,13 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction let startIndex = index; let endIndex = index; - // Move down the list and compare reportActionID with previousReportActionID + // Move up the list and compare reportActionID with previousReportActionID while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { endIndex++; } // Move up the list and compare previousReportActionID with reportActionID - while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) { + while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID || array[startIndex - 1]?.pendingAction === 'add' ) { startIndex--; } @@ -910,4 +910,4 @@ export { isReimbursementDeQueuedAction, }; -export type {LastVisibleMessage}; \ No newline at end of file +export type {LastVisibleMessage}; From 338fe4a6eebfd8d12b7c9d914859af31ef5ce1ee Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Dec 2023 09:38:37 +0100 Subject: [PATCH 026/124] explaining --- src/libs/ReportActionsUtils.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b3f46792fff3..fad712e19fbd 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -275,7 +275,7 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction if (id) { index = array.findIndex((obj) => obj.reportActionID === id); } else { - index = array.findIndex((obj) => obj.pendingAction !== 'add'); + index = array.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); } if (index === -1) { @@ -285,13 +285,22 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction let startIndex = index; let endIndex = index; - // Move up the list and compare reportActionID with previousReportActionID + // Iterate forwards through the array, starting from endIndex. This loop checks the continuity of actions by: + // 1. Comparing the current item's previousReportActionID with the next item's reportActionID. + // This ensures that we are moving in a sequence of related actions from newer to older. while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { endIndex++; } - // Move up the list and compare previousReportActionID with reportActionID - while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID || array[startIndex - 1]?.pendingAction === 'add' ) { + // Iterate backwards through the array, starting from startIndex. This loop has two main checks: + // 1. It compares the current item's reportActionID with the previous item's previousReportActionID. + // This is to ensure continuity in a sequence of actions. + // 2. If the first condition fails, it then checks if the previous item has a pendingAction of 'add'. + // This additional check is to include recently sent messages that might not yet be part of the established sequence. + while ( + (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) || + array[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD + ) { startIndex--; } From 6cf2220c0e9034b81430b565ad3f75a6b17a2de8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Dec 2023 17:36:07 +0100 Subject: [PATCH 027/124] WIP: temporary clean onyx button --- src/components/FloatingActionButton.js | 5 ++- src/libs/Permissions.ts | 2 +- .../CheckForPreviousReportActionIDClean.ts | 32 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/libs/migrations/CheckForPreviousReportActionIDClean.ts diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index 791eb150f8c9..8e5f567d8e9d 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import {Animated, Easing, View} from 'react-native'; import compose from '@libs/compose'; +import CheckForPreviousReportActionIDClean from '@libs/migrations/CheckForPreviousReportActionIDClean'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; @@ -100,7 +101,9 @@ class FloatingActionButton extends PureComponent { this.fabPressable.blur(); this.props.onPress(e); }} - onLongPress={() => {}} + onLongPress={() => { + CheckForPreviousReportActionIDClean(); + }} style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} > ): boolean { } function canUseCommentLinking(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); + return '!!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas)'; } function canUseReportFields(betas: OnyxEntry): boolean { diff --git a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts new file mode 100644 index 000000000000..7ee7a498d1a6 --- /dev/null +++ b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts @@ -0,0 +1,32 @@ +import Onyx, {OnyxCollection} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as OnyxTypes from '@src/types/onyx'; + +function getReportActionsFromOnyxClean(): Promise> { + return new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + return resolve(allReportActions); + }, + }); + }); +} + +/** + * This migration checks for the 'previousReportActionID' key in the first valid reportAction of a report in Onyx. + * If the key is not found then all reportActions for all reports are removed from Onyx. + */ +export default function (): Promise { + return getReportActionsFromOnyx().then((allReportActions) => { + const onyxData: OnyxCollection = {}; + + Object.keys(allReportActions ?? {}).forEach((onyxKey) => { + onyxData[onyxKey] = {}; + }); + + return Onyx.multiSet(onyxData as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, Record>); + }); +} From ee2b7794aae4533a72f124f180925b814819cadc Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Dec 2023 15:22:51 +0100 Subject: [PATCH 028/124] return patches --- ...eact-native+virtualized-lists+0.72.8.patch | 34 - .../react-native-web+0.19.9+001+initial.patch | 872 +++++------------- ...react-native-web+0.19.9+002+fix-mvcp.patch | 687 ++++++++++++++ ...tive-web+0.19.9+003+measureInWindow.patch} | 0 ...e-web+0.19.9+004+fix-pointer-events.patch} | 0 ...-native-web+0.19.9+005+fixLastSpacer.patch | 2 +- 6 files changed, 943 insertions(+), 652 deletions(-) delete mode 100644 patches/@react-native+virtualized-lists+0.72.8.patch create mode 100644 patches/react-native-web+0.19.9+002+fix-mvcp.patch rename patches/{react-native-web+0.19.9+002+measureInWindow.patch => react-native-web+0.19.9+003+measureInWindow.patch} (100%) rename patches/{react-native-web+0.19.9+003+fix-pointer-events.patch => react-native-web+0.19.9+004+fix-pointer-events.patch} (100%) diff --git a/patches/@react-native+virtualized-lists+0.72.8.patch b/patches/@react-native+virtualized-lists+0.72.8.patch deleted file mode 100644 index a3bef95f1618..000000000000 --- a/patches/@react-native+virtualized-lists+0.72.8.patch +++ /dev/null @@ -1,34 +0,0 @@ -diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -index ef5a3f0..2590edd 100644 ---- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -+++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -@@ -125,19 +125,6 @@ function windowSizeOrDefault(windowSize: ?number) { - return windowSize ?? 21; - } - --function findLastWhere( -- arr: $ReadOnlyArray, -- predicate: (element: T) => boolean, --): T | null { -- for (let i = arr.length - 1; i >= 0; i--) { -- if (predicate(arr[i])) { -- return arr[i]; -- } -- } -- -- return null; --} -- - /** - * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) - * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better -@@ -1019,7 +1006,8 @@ class VirtualizedList extends StateSafePureComponent { - const spacerKey = this._getSpacerKey(!horizontal); - - const renderRegions = this.state.renderMask.enumerateRegions(); -- const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); -+ const lastRegion = renderRegions[renderRegions.length - 1]; -+ const lastSpacer = lastRegion?.isSpacer ? lastRegion : null; - - for (const section of renderRegions) { - if (section.isSpacer) { diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch index 91ba6bfd59c0..d88ef83d4bcd 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -1,648 +1,286 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..0c9dfcb 100644 +index c879838..288316c 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] - - constructor(_props) { -- var _this$props$updateCel; -+ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; - super(_props); - this._getScrollMetrics = () => { - return this._scrollMetrics; -@@ -520,6 +520,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -569,7 +574,7 @@ class VirtualizedList extends StateSafePureComponent { - this._updateCellsToRender = () => { - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - this.setState((state, props) => { -- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); -+ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); - var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); - if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { - return null; -@@ -589,7 +594,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable - }; - }; -@@ -621,12 +626,10 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._getFrameMetrics = (index, props) => { - var data = props.data, -- getItem = props.getItem, - getItemCount = props.getItemCount, - getItemLayout = props.getItemLayout; - invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); -- var item = getItem(data, index); -- var frame = this._frames[this._keyExtractor(item, index, props)]; -+ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -650,7 +653,7 @@ class VirtualizedList extends StateSafePureComponent { - - // The last cell we rendered may be at a new index. Bail if we don't know - // where it is. -- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { -+ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { - return []; - } - var first = focusedCellIndex; -@@ -690,9 +693,15 @@ class VirtualizedList extends StateSafePureComponent { - } - } - var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); -+ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; - this.state = { - cellsAroundViewport: initialRenderRegion, -- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) -+ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), -+ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -748,6 +757,26 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -+ static _findItemIndexWithKey(props, key, hint) { -+ var itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ var curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (var ii = 0; ii < itemCount; ii++) { -+ var _curKey = VirtualizedList._getItemKey(props, ii); -+ if (_curKey === key) { -+ return ii; -+ } +@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { + * + */ + class VirtualizedList extends StateSafePureComponent { ++ pushOrUnshift(input, item) { ++ if (this.props.inverted) { ++ input.unshift(item); ++ } else { ++ input.push(item); + } -+ return null; + } -+ static _getItemKey(props, index) { -+ var item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } - static _createRenderMask(props, cellsAroundViewport, additionalRegions) { - var itemCount = props.getItemCount(props.data); - invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); -@@ -796,7 +825,7 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -- _adjustCellsAroundViewport(props, cellsAroundViewport) { -+ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { - var data = props.data, - getItemCount = props.getItemCount; - var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); -@@ -819,17 +848,9 @@ class VirtualizedList extends StateSafePureComponent { - last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; ++ + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params) { + var animated = params ? params.animated : true; +@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._defaultRenderScrollComponent = props => { + var onRefresh = props.onRefresh; ++ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return /*#__PURE__*/React.createElement(View, props); +@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { + refreshing: props.refreshing, + onRefresh: onRefresh, + progressViewOffset: props.progressViewOffset +- }) : props.refreshControl ++ }) : props.refreshControl, ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] + })) + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return /*#__PURE__*/React.createElement(ScrollView, props); ++ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] ++ })); } - newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); -@@ -902,16 +923,36 @@ class VirtualizedList extends StateSafePureComponent { - } - } - static getDerivedStateFromProps(newProps, prevState) { -+ var _newProps$maintainVis, _newProps$maintainVis2; - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - var itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } -- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); -+ var maintainVisibleContentPositionAdjustment = null; -+ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; -+ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; -+ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); -+ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { -+ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, -+ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment -+ } : prevState.cellsAroundViewport, newProps); - return { - cellsAroundViewport: constrainedCells, -- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) -+ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount }; - } - _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -934,7 +975,7 @@ class VirtualizedList extends StateSafePureComponent { - last = Math.min(end, last); - var _loop = function _loop() { - var item = getItem(data, ii); -- var key = _this._keyExtractor(item, ii, _this.props); -+ var key = VirtualizedList._keyExtractor(item, ii, _this.props); + this._onCellLayout = (e, cellKey, index) => { +@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { + onViewableItemsChanged = _this$props3.onViewableItemsChanged, + viewabilityConfig = _this$props3.viewabilityConfig; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged + }); +@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { + var key = _this._keyExtractor(item, ii, _this.props); _this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { - stickyHeaderIndices.push(cells.length); -@@ -969,20 +1010,23 @@ class VirtualizedList extends StateSafePureComponent { - } - static _constrainToItemCount(cells, props) { - var itemCount = props.getItemCount(props.data); -- var last = Math.min(itemCount - 1, cells.last); -+ var lastPossibleCellIndex = itemCount - 1; -+ -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); -+ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last) - }; - } - _isNestedWithSameOrientation() { - var nestedContext = this.context; - return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); - } -- _keyExtractor(item, index, props -- // $FlowFixMe[missing-local-annot] -- ) { -+ static _keyExtractor(item, index, props) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -1022,7 +1066,12 @@ class VirtualizedList extends StateSafePureComponent { - cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { +- stickyHeaderIndices.push(cells.length); ++ _this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); +- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ ++ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ + CellRendererComponent: CellRendererComponent, + ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, + ListItemComponent: ListItemComponent, +@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : + /*#__PURE__*/ + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListHeaderComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + '-header', key: "$header" -- }, /*#__PURE__*/React.createElement(View, { -+ }, /*#__PURE__*/React.createElement(View -+ // We expect that header component will be a single native view so make it -+ // not collapsable to avoid this view being flattened and make this assumption -+ // no longer true. -+ , { -+ collapsable: false, - onLayout: this._onLayoutHeader, - style: [inversionStyle, this.props.ListHeaderComponentStyle] - }, -@@ -1124,7 +1173,11 @@ class VirtualizedList extends StateSafePureComponent { - // TODO: Android support - invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, - stickyHeaderIndices, -- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style -+ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, -+ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) -+ }) : undefined - }); - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReached = _this$props8.onStartReached, - onStartReachedThreshold = _this$props8.onStartReachedThreshold, - onEndReached = _this$props8.onEndReached, -- onEndReachedThreshold = _this$props8.onEndReachedThreshold, -- initialScrollIndex = _this$props8.initialScrollIndex; -+ onEndReachedThreshold = _this$props8.onEndReachedThreshold; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - var _this$_scrollMetrics2 = this._scrollMetrics, - contentLength = _this$_scrollMetrics2.contentLength, - visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({ -- distanceFromStart -- }); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({ -+ distanceFromStart -+ }); - } - - // If the user scrolls away from the start or end and back again, -@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { + }, /*#__PURE__*/React.createElement(View, { +@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListEmptyComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-empty', + key: "$empty" + }, /*#__PURE__*/React.cloneElement(_element2, { +@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { + var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); + var lastMetrics = this.__getFrameMetricsApprox(last, this.props); + var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( /*#__PURE__*/React.createElement(View, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { + key: "$spacer-" + section.first, + style: { + [spacerKey]: spacerSize +@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListFooterComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getFooterCellKey(), + key: "$footer" + }, /*#__PURE__*/React.createElement(View, { +@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } } + var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; +@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ + left: 0, + borderColor: 'red', + borderWidth: 2 ++ }, ++ rowReverse: { ++ flexDirection: 'row-reverse' ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse' } - _updateViewableItems(props, cellsAroundViewport) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); - }); + }); + export default VirtualizedList; +\ No newline at end of file diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..43f9653 100644 +index c7d68bb..46b3fc9 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { - type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -+ // Used to track items added at the start of the list for maintainVisibleContentPosition. -+ firstVisibleItemKey: ?string, -+ // When > 0 the scroll position available in JS is considered stale and should not be used. -+ pendingScrollUpdateCount: number, - }; - - /** -@@ -447,9 +451,24 @@ class VirtualizedList extends StateSafePureComponent { - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); +@@ -167,6 +167,14 @@ function findLastWhere( + class VirtualizedList extends StateSafePureComponent { + static contextType: typeof VirtualizedListContext = VirtualizedListContext; -+ const minIndexForVisible = -+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), -+ firstVisibleItemKey: -+ this.props.getItemCount(this.props.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) -+ : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: -+ this.props.initialScrollIndex != null && -+ this.props.initialScrollIndex > 0 -+ ? 1 -+ : 0, - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -534,6 +553,40 @@ class VirtualizedList extends StateSafePureComponent { - } - } - -+ static _findItemIndexWithKey( -+ props: Props, -+ key: string, -+ hint: ?number, -+ ): ?number { -+ const itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ const curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } ++ pushOrUnshift(input: Array, item: Item) { ++ if (this.props.inverted) { ++ input.unshift(item) ++ } else { ++ input.push(item) + } -+ for (let ii = 0; ii < itemCount; ii++) { -+ const curKey = VirtualizedList._getItemKey(props, ii); -+ if (curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ -+ static _getItemKey( -+ props: { -+ data: Props['data'], -+ getItem: Props['getItem'], -+ keyExtractor: Props['keyExtractor'], -+ ... -+ }, -+ index: number, -+ ): string { -+ const item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); + } + - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, -@@ -617,6 +670,7 @@ class VirtualizedList extends StateSafePureComponent { - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, -+ pendingScrollUpdateCount: number, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( -@@ -648,21 +702,9 @@ class VirtualizedList extends StateSafePureComponent { - ), - }; + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + const animated = params ? params.animated : true; +@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if ( -- props.initialScrollIndex && -- !this._scrollMetrics.offset && -- Math.abs(distanceFromEnd) >= Number.EPSILON -- ) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; -@@ -771,14 +813,59 @@ class VirtualizedList extends StateSafePureComponent { - return prevState; - } - -+ let maintainVisibleContentPositionAdjustment: ?number = null; -+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ const minIndexForVisible = -+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ const newFirstVisibleItemKey = -+ newProps.getItemCount(newProps.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) -+ : null; -+ if ( -+ newProps.maintainVisibleContentPosition != null && -+ prevFirstVisibleItemKey != null && -+ newFirstVisibleItemKey != null -+ ) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ const hint = -+ itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( -+ newProps, -+ prevFirstVisibleItemKey, -+ hint, -+ ); -+ maintainVisibleContentPositionAdjustment = -+ firstVisibleItemIndex != null -+ ? firstVisibleItemIndex - minIndexForVisible -+ : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ - const constrainedCells = VirtualizedList._constrainToItemCount( -- prevState.cellsAroundViewport, -+ maintainVisibleContentPositionAdjustment != null -+ ? { -+ first: -+ prevState.cellsAroundViewport.first + -+ maintainVisibleContentPositionAdjustment, -+ last: -+ prevState.cellsAroundViewport.last + -+ maintainVisibleContentPositionAdjustment, -+ } -+ : prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: -+ maintainVisibleContentPositionAdjustment != null -+ ? prevState.pendingScrollUpdateCount + 1 -+ : prevState.pendingScrollUpdateCount, - }; - } - -@@ -810,7 +897,7 @@ class VirtualizedList extends StateSafePureComponent { - - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); -- const key = this._keyExtractor(item, ii, this.props); -+ const key = VirtualizedList._keyExtractor(item, ii, this.props); + const {onViewableItemsChanged, viewabilityConfig} = this.props; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged, + }); +@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -@@ -853,15 +940,19 @@ class VirtualizedList extends StateSafePureComponent { - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); -- const last = Math.min(itemCount - 1, cells.last); -+ const lastPossibleCellIndex = itemCount - 1; - -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); -+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last, -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last), - }; - } +- stickyHeaderIndices.push(cells.length); ++ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); + } -@@ -883,15 +974,14 @@ class VirtualizedList extends StateSafePureComponent { - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; + const shouldListenForLayout = + getItemLayout == null || debug || this._fillRateHelper.enabled(); -- _keyExtractor( -+ static _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, -- // $FlowFixMe[missing-local-annot] -- ) { -+ ): string { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -937,6 +1027,10 @@ class VirtualizedList extends StateSafePureComponent { +- cells.push( ++ this.pushOrUnshift(cells, + { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent +@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + ); +- cells.push( ++ this.pushOrUnshift(cells, + - { - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, -+ maintainVisibleContentPosition: -+ this.props.maintainVisibleContentPosition != null -+ ? { -+ ...this.props.maintainVisibleContentPosition, -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: -+ this.props.maintainVisibleContentPosition.minIndexForVisible + -+ (this.props.ListHeaderComponent ? 1 : 0), -+ } -+ : undefined, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; -@@ -1516,8 +1620,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, -- initialScrollIndex, - } = this.props; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; -@@ -1569,14 +1677,8 @@ class VirtualizedList extends StateSafePureComponent { - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({distanceFromStart}); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({distanceFromStart}); - } - - // If the user scrolls away from the start or end and back again, -@@ -1703,6 +1805,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale, - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -1818,6 +1925,7 @@ class VirtualizedList extends StateSafePureComponent { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, -+ state.pendingScrollUpdateCount, +@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + )): any); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { + const lastMetrics = this.__getFrameMetricsApprox(last, this.props); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( ++ this.pushOrUnshift(cells, + { + // $FlowFixMe[incompatible-type-arg] + ); - const renderMask = VirtualizedList._createRenderMask( - props, -@@ -1848,7 +1956,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable, - }; +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { + * LTI update could not be added via codemod */ + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; ++ const inversionStyle = this.props.inverted ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; ++ + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { + props.refreshControl + ) + } ++ contentContainerStyle={[ ++ inversionStyle, ++ this.props.contentContainerStyle, ++ ]} + /> + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return ; ++ return ( ++ ++ ); + } }; -@@ -1909,13 +2017,12 @@ class VirtualizedList extends StateSafePureComponent { - inLayout?: boolean, - ... - } => { -- const {data, getItem, getItemCount, getItemLayout} = props; -+ const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); -- const item = getItem(data, index); -- const frame = this._frames[this._keyExtractor(item, index, props)]; -+ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -1950,11 +2057,8 @@ class VirtualizedList extends StateSafePureComponent { - // where it is. - if ( - focusedCellIndex >= itemCount || -- this._keyExtractor( -- props.getItem(props.data, focusedCellIndex), -- focusedCellIndex, -- props, -- ) !== this._lastFocusedCellKey -+ VirtualizedList._getItemKey(props, focusedCellIndex) !== -+ this._lastFocusedCellKey - ) { - return []; + +@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } } -@@ -1995,6 +2099,11 @@ class VirtualizedList extends StateSafePureComponent { - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, + const windowTop = this.__getFrameMetricsApprox( +@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ + borderColor: 'red', + borderWidth: 2, + }, ++ rowReverse: { ++ flexDirection: 'row-reverse', ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse', ++ }, + }); + + export default VirtualizedList; +\ No newline at end of file diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch new file mode 100644 index 000000000000..afd681bba3b0 --- /dev/null +++ b/patches/react-native-web+0.19.9+002+fix-mvcp.patch @@ -0,0 +1,687 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index a6fe142..faeb323 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { + var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; + var isEventTargetScrollable = scrollLength > clientLength; + var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; +- var leftoverDelta = delta; ++ var leftoverDelta = delta * 0.5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); + } +@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; + } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount + }; + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); + _this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + _this.pushOrUnshift(stickyHeaderIndices, cells.length); +@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { + this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-header', + key: "$header" +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { + } + } + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index d896fb1..f303b31 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { + const delta = this.props.horizontal + ? ev.deltaX || ev.wheelDeltaX + : ev.deltaY || ev.wheelDeltaY; +- let leftoverDelta = delta; ++ let leftoverDelta = delta * 5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 + ? Math.min(delta + scrollOffset, 0) +@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } ++ + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); + + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); ++ this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + + const shouldListenForLayout = +@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; + ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last, ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last), + }; + } + +@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { + cellKey={this._getCellKey() + '-header'} + key="$header"> + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; + + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; + const inversionStyle = this.props.inverted +- ? this.props.horizontal +- ? styles.rowReverse +- : styles.columnReverse +- : null; +- ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, +@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/patches/react-native-web+0.19.9+002+measureInWindow.patch b/patches/react-native-web+0.19.9+003+measureInWindow.patch similarity index 100% rename from patches/react-native-web+0.19.9+002+measureInWindow.patch rename to patches/react-native-web+0.19.9+003+measureInWindow.patch diff --git a/patches/react-native-web+0.19.9+003+fix-pointer-events.patch b/patches/react-native-web+0.19.9+004+fix-pointer-events.patch similarity index 100% rename from patches/react-native-web+0.19.9+003+fix-pointer-events.patch rename to patches/react-native-web+0.19.9+004+fix-pointer-events.patch diff --git a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch b/patches/react-native-web+0.19.9+005+fixLastSpacer.patch index fc48c00094dc..0ca5ac778e0b 100644 --- a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch +++ b/patches/react-native-web+0.19.9+005+fixLastSpacer.patch @@ -26,4 +26,4 @@ index faeb323..68d740a 100644 + var lastSpacer = lastRegion?.isSpacer ? lastRegion : null; for (var _iterator = _createForOfIteratorHelperLoose(renderRegions), _step; !(_step = _iterator()).done;) { var section = _step.value; - if (section.isSpacer) { + if (section.isSpacer) { \ No newline at end of file From 8196979763809e16bda6f198275b75737047aed9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Dec 2023 15:24:33 +0100 Subject: [PATCH 029/124] block fetching newer actions if the screen size is too large --- .../CheckForPreviousReportActionIDClean.ts | 2 +- src/pages/home/report/ReportActionsList.js | 12 ++- src/pages/home/report/ReportActionsView.js | 75 +++++++++++++------ 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts index 7ee7a498d1a6..4362ae79114b 100644 --- a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts +++ b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts @@ -2,7 +2,7 @@ import Onyx, {OnyxCollection} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import * as OnyxTypes from '@src/types/onyx'; -function getReportActionsFromOnyxClean(): Promise> { +function getReportActionsFromOnyx(): Promise> { return new Promise((resolve) => { const connectionID = Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 3df1ce98855c..24ac03ce1f87 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -12,6 +12,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; @@ -139,6 +140,7 @@ function ReportActionsList({ isComposerFullSize, reportScrollManager, listID, + onContentSizeChange, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -440,6 +442,12 @@ function ReportActionsList({ }, [onLayout], ); + const onContentSizeChangeInner = useCallback( + (w, h) => { + onContentSizeChange(w,h) + }, + [onContentSizeChange], + ); const listHeaderComponent = useCallback(() => { if (!isOffline && !hasHeaderRendered.current) { @@ -471,7 +479,8 @@ function ReportActionsList({ renderItem={renderItem} contentContainerStyle={contentContainerStyle} keyExtractor={keyExtractor} - initialNumToRender={initialNumToRender} + // initialNumToRender={initialNumToRender} + initialNumToRender={50} onEndReached={loadOlderChats} onEndReachedThreshold={0.75} onStartReached={loadNewerChats} @@ -480,6 +489,7 @@ function ReportActionsList({ ListHeaderComponent={listHeaderComponent} keyboardShouldPersistTaps="handled" onLayout={onLayoutInner} + onContentSizeChange={onContentSizeChangeInner} onScroll={trackVerticalScrolling} onScrollToIndexFailed={() => {}} extraData={extraData} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3a6dd00c0699..6b5b86a1950b 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -16,6 +16,7 @@ import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; +// import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -100,6 +101,7 @@ function getReportActionID(route) { const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { const [edgeID, setEdgeID] = useState(linkedID); const [listID, setListID] = useState(1); + const isFirstRender = useRef(true); const index = useMemo(() => { if (!linkedID) { @@ -110,6 +112,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe }, [messageArray, linkedID, edgeID]); useMemo(() => { + isFirstRender.current = true; setEdgeID(''); }, [route, linkedID]); @@ -117,9 +120,9 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe if (!linkedID || index === -1) { return messageArray; } - - if (linkedID && !edgeID) { + if ((linkedID && !edgeID) || (linkedID && isFirstRender.current)) { setListID((i) => i + 1); + isFirstRender.current = false; return messageArray.slice(index, messageArray.length); } else if (linkedID && edgeID) { const amountOfItemsBeforeLinkedOne = 10; @@ -163,9 +166,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; + const contentListHeight = useRef(0); + const layoutListHeight = useRef(0); + const isInitial = useRef(true); + // const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + // const {windowHeight} = useWindowDimensions(); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -174,6 +181,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const isFocused = useIsFocused(); const reportID = props.report.reportID; + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. @@ -191,10 +199,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], ); - const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, isLoadingLinkedMessage); + const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route); const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); const newestReportAction = lodashGet(reportActions, '[0]'); + const oldestReportAction = _.last(reportActions); + const isWeReachedTheOldestAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; /** * @returns {Boolean} @@ -211,6 +221,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro }; useEffect(() => { + if (reportActionID) { + return; + } openReportIfNecessary(); InteractionManager.runAfterInteractions(() => { @@ -282,6 +295,10 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } }, [props.report, didSubscribeToReportTypingEvents, reportID]); + const onContentSizeChange = useCallback((w, h) => { + contentListHeight.current = h; + }, []); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. @@ -292,10 +309,8 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - const oldestReportAction = _.last(reportActions); - // Don't load more chats if we're already at the beginning of the chat history - if (!oldestReportAction || oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + if (!oldestReportAction || isWeReachedTheOldestAction) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments @@ -306,32 +321,47 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return ({distanceFromStart}) => { - if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction) || (!reportActionID && !hasNewestReportAction)) { + // const shouldFirstlyLoadOlderActions = Number(layoutListHeight.current) > Number(contentListHeight.current); + // const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; + // const SPACER = 30; + // const MIN_PREDEFINED_PADDING = 16; + // const isListSmallerThanScreen = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; + // const isListEmpty = contentListHeight.current === MIN_PREDEFINED_PADDING; + // const shouldFirstlyLoadOlderActions = !isWeReachedTheOldestAction && isListSmallerThanScreen + // const shouldFirstlyLoadOlderActions = !isListSmallerThanScreen + if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current) || (!reportActionID && !hasNewestReportAction)) { fetchFunc({firstReportActionID, distanceFromStart}); } + isInitial.current = false; }, + // [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight, isWeReachedTheOldestAction], [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID], ); /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = () => { - if (didLayout.current) { - return; - } + const recordTimeToMeasureItemLayout = useCallback( + (e) => { + layoutListHeight.current = e.nativeEvent.layout.height; - didLayout.current = true; - Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); + if (didLayout.current) { + return; + } - // Capture the init measurement only once not per each chat switch as the value gets overwritten - if (!ReportActionsView.initMeasured) { - Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); - ReportActionsView.initMeasured = true; - } else { - Performance.markEnd(CONST.TIMING.SWITCH_REPORT); - } - }; + didLayout.current = true; + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); + + // Capture the init measurement only once not per each chat switch as the value gets overwritten + if (!ReportActionsView.initMeasured) { + Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); + ReportActionsView.initMeasured = true; + } else { + Performance.markEnd(CONST.TIMING.SWITCH_REPORT); + } + }, + [hasCachedActions], + ); // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { @@ -354,6 +384,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro reportScrollManager={reportScrollManager} policy={props.policy} listID={listID} + onContentSizeChange={onContentSizeChange} /> From 7f94dedc55023093419f1447e37a9c49cdd60887 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 23 Dec 2023 10:38:34 +0100 Subject: [PATCH 030/124] fix bottom loader for invisible actions --- src/libs/ReportActionsUtils.ts | 26 +++++++++++++------------- src/pages/home/ReportScreen.js | 12 ++++-------- tests/unit/ReportActionsUtilsTest.js | 2 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fad712e19fbd..f5f2637d4f2d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -183,7 +183,7 @@ function isTransactionThread(parentReportAction: OnyxEntry): boole * This gives us a stable order even in the case of multiple reportActions created on the same millisecond * */ -function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { +function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] { if (!Array.isArray(reportActions)) { throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`); } @@ -211,14 +211,6 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier; }); - // If shouldMarkTheFirstItemAsNewest is true, label the first reportAction as isNewestReportAction - if (shouldMarkTheFirstItemAsNewest && sortedActions?.length > 0) { - sortedActions[0] = { - ...sortedActions[0], - isNewestReportAction: true, - }; - } - return sortedActions; } @@ -566,19 +558,27 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { const filteredReportActions = Object.entries(reportActions ?? {}) // .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); - return getSortedReportActions(baseURLAdjustedReportActions, true, shouldMarkTheFirstItemAsNewest); + return getSortedReportActions(baseURLAdjustedReportActions, true); } -function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { +function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { if (!reportActions) { return []; } - return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); + const filtered = reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); + + if (shouldMarkTheFirstItemAsNewest && filtered?.length > 0) { + filtered[0] = { + ...filtered[0], + isNewestReportAction: true, + }; + } + return filtered; } /** diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 3b442f975f5a..6c223b89e09e 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -66,9 +66,6 @@ const propTypes = { /** The report metadata loading states */ reportMetadata: reportMetadataPropTypes, - /** Array of report actions for this report */ - sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), - /** Whether the composer is full size */ isComposerFullSize: PropTypes.bool, @@ -104,7 +101,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - sortedReportActions: [], + // sortedReportActions: [], report: {}, reportMetadata: { isLoadingInitialReportActions: true, @@ -181,9 +178,9 @@ function ReportScreen({ const reportActions = useMemo(() => { if (allReportActions?.length === 0) return []; - const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); - const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sorterReportActions, reportActionID); - const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); + const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions, true); return reportActionsWithoutDeleted; }, [reportActionID, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); @@ -199,7 +196,6 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); 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) && reportMetadata.isLoadingInitialReportActions; diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index 545d442e4799..6e7620b75d76 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -239,7 +239,7 @@ describe('ReportActionsUtils', () => { ]; const resultWithoutNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input); - const resultWithNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); + const resultWithNewestFlag = ReportActionsUtils.getReportActionsWithoutRemoved(input, true); input.pop(); // Mark the newest report action as the newest report action resultWithoutNewestFlag[0] = { From 91b8a48d3f3ec03ff1fb9aeb3d945b19ac249986 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:09:16 +0100 Subject: [PATCH 031/124] run hover after interaction --- src/components/Hoverable/index.tsx | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 9c641cfc19be..eba0542a2d38 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,5 +1,5 @@ import React, {ForwardedRef, forwardRef, MutableRefObject, ReactElement, RefAttributes, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; +import {DeviceEventEmitter, InteractionManager} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import HoverableProps from './types'; @@ -41,16 +41,41 @@ function assignRef(ref: ((instance: HTMLElement | null) => void) | MutableRefObj } } +type UseHoveredReturnType = [boolean, (newValue: boolean) => void]; +function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): UseHoveredReturnType { + const [state, setState] = useState(initialValue); + + const interceptedSetState = useCallback((newValue: boolean) => { + if (runHoverAfterInteraction) { + InteractionManager.runAfterInteractions(() => { + setState(newValue); + }); + } else { + setState(newValue); + } + }, []); + return [state, interceptedSetState]; +} + /** * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state, * because nesting Pressables causes issues where the hovered state of the child cannot be easily propagated to the * parent. https://github.com/necolas/react-native-web/issues/1875 */ function Hoverable( - {disabled = false, onHoverIn = () => {}, onHoverOut = () => {}, onMouseEnter = () => {}, onMouseLeave = () => {}, children, shouldHandleScroll = false}: HoverableProps, + { + disabled = false, + onHoverIn = () => {}, + onHoverOut = () => {}, + onMouseEnter = () => {}, + onMouseLeave = () => {}, + children, + shouldHandleScroll = false, + runHoverAfterInteraction = false, + }: HoverableProps, outerRef: ForwardedRef, ) { - const [isHovered, setIsHovered] = useState(false); + const [isHovered, setIsHovered] = useHovered(false, runHoverAfterInteraction); const isScrolling = useRef(false); const isHoveredRef = useRef(false); From 3f652b2348ed4df85b5bd598ee1edb3c468bc350 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:09:29 +0100 Subject: [PATCH 032/124] add types --- src/components/Hoverable/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 430b865f50c5..99b26f2ee10a 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -21,6 +21,9 @@ type HoverableProps = { /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ shouldHandleScroll?: boolean; + + /** Call setHovered(true) with runAfterInteraction */ + runHoverAfterInteraction?: boolean; }; export default HoverableProps; From 7ab464f2aae858aa892fa8ff87ca5f239b546475 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:11:31 +0100 Subject: [PATCH 033/124] remove shouldMarkTheFirstItemAsNewest --- src/libs/ReportActionsUtils.ts | 12 ++---------- src/pages/home/ReportScreen.js | 5 ++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f5f2637d4f2d..e4892fdfef32 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -566,19 +566,11 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): return getSortedReportActions(baseURLAdjustedReportActions, true); } -function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { +function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { if (!reportActions) { return []; } - const filtered = reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); - - if (shouldMarkTheFirstItemAsNewest && filtered?.length > 0) { - filtered[0] = { - ...filtered[0], - isNewestReportAction: true, - }; - } - return filtered; + return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); } /** diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 6c223b89e09e..1dd5f1caf582 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -39,7 +39,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import HeaderView from './HeaderView'; -import reportActionPropTypes from './report/reportActionPropTypes'; import ReportActionsView from './report/ReportActionsView'; import ReportFooter from './report/ReportFooter'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; @@ -180,7 +179,7 @@ function ReportScreen({ if (allReportActions?.length === 0) return []; const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions, true); + const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); return reportActionsWithoutDeleted; }, [reportActionID, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); @@ -490,7 +489,7 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && !isLoading && ( + {isReportReadyForDisplay && ( Date: Wed, 27 Dec 2023 21:11:57 +0100 Subject: [PATCH 034/124] add runHoverAfterInteraction to report --- src/pages/home/report/ReportActionItem.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index c81e47016dcc..77d269d799eb 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -677,6 +677,7 @@ function ReportActionItem(props) { > {(hovered) => ( From d594cec109a0e9fe59dfdb2fa338396ad03c7005 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:13:55 +0100 Subject: [PATCH 035/124] adjust the chat cutting for proper layout --- src/pages/home/report/ReportActionsView.js | 49 +++++++++++----------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 6b5b86a1950b..1a28b222c4e9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -16,7 +16,7 @@ import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; -// import useWindowDimensions from '@hooks/useWindowDimensions'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -100,7 +100,7 @@ function getReportActionID(route) { const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { const [edgeID, setEdgeID] = useState(linkedID); - const [listID, setListID] = useState(1); + const [listID, setListID] = useState(() => Math.round(Math.random() * 100)); const isFirstRender = useRef(true); const index = useMemo(() => { @@ -116,21 +116,18 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe setEdgeID(''); }, [route, linkedID]); + const cattedArray = useMemo(() => { if (!linkedID || index === -1) { return messageArray; } - if ((linkedID && !edgeID) || (linkedID && isFirstRender.current)) { + if (isFirstRender.current) { setListID((i) => i + 1); - isFirstRender.current = false; return messageArray.slice(index, messageArray.length); - } else if (linkedID && edgeID) { + } else if (edgeID) { const amountOfItemsBeforeLinkedOne = 10; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - if (index) { - return messageArray.slice(newStartIndex, messageArray.length); - } - return messageArray; + return messageArray.slice(newStartIndex, messageArray.length); } return messageArray; }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); @@ -145,7 +142,14 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe fetchFn({distanceFromStart}); } - setEdgeID(firstReportActionID); + if (isFirstRender.current) { + isFirstRender.current = false; + InteractionManager.runAfterInteractions(() => { + setEdgeID(firstReportActionID); + }); + } else { + setEdgeID(firstReportActionID); + } }, [setEdgeID, fetchFn, hasMoreCashed], ); @@ -169,10 +173,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const contentListHeight = useRef(0); const layoutListHeight = useRef(0); const isInitial = useRef(true); - // const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - // const {windowHeight} = useWindowDimensions(); + const {windowHeight} = useWindowDimensions(); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -201,7 +204,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route); - const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); + const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = _.last(reportActions); const isWeReachedTheOldestAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; @@ -321,21 +324,19 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return ({distanceFromStart}) => { - // const shouldFirstlyLoadOlderActions = Number(layoutListHeight.current) > Number(contentListHeight.current); - // const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; - // const SPACER = 30; - // const MIN_PREDEFINED_PADDING = 16; - // const isListSmallerThanScreen = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; - // const isListEmpty = contentListHeight.current === MIN_PREDEFINED_PADDING; - // const shouldFirstlyLoadOlderActions = !isWeReachedTheOldestAction && isListSmallerThanScreen - // const shouldFirstlyLoadOlderActions = !isListSmallerThanScreen - if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current) || (!reportActionID && !hasNewestReportAction)) { + const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; + const SPACER = 30; + const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; + + if ( + (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current && !isContentSmallerThanList) || + (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList) + ) { fetchFunc({firstReportActionID, distanceFromStart}); } isInitial.current = false; }, - // [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight, isWeReachedTheOldestAction], - [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID], + [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], ); /** From 96a1d42b2a3930e442d7d9cc392f76e0f1864b33 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:45:50 +0100 Subject: [PATCH 036/124] bring back 'Inverted fix' --- .../react-native-web+0.19.9+001+initial.patch | 872 +++++++++++++----- ...react-native-web+0.19.9+002+fix-mvcp.patch | 687 -------------- ...tive-web+0.19.9+002+measureInWindow.patch} | 0 ...e-web+0.19.9+003+fix-pointer-events.patch} | 0 ...-native-web+0.19.9+005+fixLastSpacer.patch | 29 - 5 files changed, 617 insertions(+), 971 deletions(-) delete mode 100644 patches/react-native-web+0.19.9+002+fix-mvcp.patch rename patches/{react-native-web+0.19.9+003+measureInWindow.patch => react-native-web+0.19.9+002+measureInWindow.patch} (100%) rename patches/{react-native-web+0.19.9+004+fix-pointer-events.patch => react-native-web+0.19.9+003+fix-pointer-events.patch} (100%) delete mode 100644 patches/react-native-web+0.19.9+005+fixLastSpacer.patch diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch index d88ef83d4bcd..91ba6bfd59c0 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -1,286 +1,648 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..288316c 100644 +index c879838..0c9dfcb 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { - * - */ - class VirtualizedList extends StateSafePureComponent { -+ pushOrUnshift(input, item) { -+ if (this.props.inverted) { -+ input.unshift(item); -+ } else { -+ input.push(item); +@@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -520,6 +520,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -569,7 +574,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -589,7 +594,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -621,12 +626,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -650,7 +653,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -690,9 +693,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -748,6 +757,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } + } ++ return null; + } -+ - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params) { - var animated = params ? params.animated : true; -@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._defaultRenderScrollComponent = props => { - var onRefresh = props.onRefresh; -+ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return /*#__PURE__*/React.createElement(View, props); -@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { - refreshing: props.refreshing, - onRefresh: onRefresh, - progressViewOffset: props.progressViewOffset -- }) : props.refreshControl -+ }) : props.refreshControl, -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] - })) - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return /*#__PURE__*/React.createElement(ScrollView, props); -+ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] -+ })); ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -796,7 +825,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -819,17 +848,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -902,16 +923,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount }; - this._onCellLayout = (e, cellKey, index) => { -@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { - onViewableItemsChanged = _this$props3.onViewableItemsChanged, - viewabilityConfig = _this$props3.viewabilityConfig; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged - }); -@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { - var key = _this._keyExtractor(item, ii, _this.props); + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -934,7 +975,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); _this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ _this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); -- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ -+ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ - CellRendererComponent: CellRendererComponent, - ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, - ListItemComponent: ListItemComponent, -@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : - /*#__PURE__*/ - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListHeaderComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + stickyHeaderIndices.push(cells.length); +@@ -969,20 +1010,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1022,7 +1066,12 @@ class VirtualizedList extends StateSafePureComponent { + cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + '-header', key: "$header" - }, /*#__PURE__*/React.createElement(View, { -@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListEmptyComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-empty', - key: "$empty" - }, /*#__PURE__*/React.cloneElement(_element2, { -@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { - var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); - var lastMetrics = this.__getFrameMetricsApprox(last, this.props); - var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( /*#__PURE__*/React.createElement(View, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { - key: "$spacer-" + section.first, - style: { - [spacerKey]: spacerSize -@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListFooterComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getFooterCellKey(), - key: "$footer" - }, /*#__PURE__*/React.createElement(View, { -@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1124,7 +1173,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { } - var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; -@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ - left: 0, - borderColor: 'red', - borderWidth: 2 -+ }, -+ rowReverse: { -+ flexDirection: 'row-reverse' -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse' } - }); - export default VirtualizedList; -\ No newline at end of file + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..46b3fc9 100644 +index c7d68bb..43f9653 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -167,6 +167,14 @@ function findLastWhere( - class VirtualizedList extends StateSafePureComponent { - static contextType: typeof VirtualizedListContext = VirtualizedListContext; +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -447,9 +451,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); -+ pushOrUnshift(input: Array, item: Item) { -+ if (this.props.inverted) { -+ input.unshift(item) -+ } else { -+ input.push(item) ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -534,6 +553,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } + } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); + } + - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - const animated = params ? params.animated : true; -@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -617,6 +670,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -648,21 +702,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; } else { - const {onViewableItemsChanged, viewabilityConfig} = this.props; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged, - }); -@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -771,14 +813,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -810,7 +897,7 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); - } +@@ -853,15 +940,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; - const shouldListenForLayout = - getItemLayout == null || debug || this._fillRateHelper.enabled(); ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); -- cells.push( -+ this.pushOrUnshift(cells, - { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent -@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -937,6 +1027,10 @@ class VirtualizedList extends StateSafePureComponent { cellKey={this._getCellKey() + '-header'} key="$header"> -@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - )): any); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { - const lastMetrics = this.__getFrameMetricsApprox(last, this.props); - const spacerSize = - lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( -+ this.pushOrUnshift(cells, - { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { - * LTI update could not be added via codemod */ - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; -+ const inversionStyle = this.props.inverted -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; -+ - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { - props.refreshControl - ) - } -+ contentContainerStyle={[ -+ inversionStyle, -+ this.props.contentContainerStyle, -+ ]} - /> - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return ; -+ return ( -+ -+ ); - } - }; + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; -@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1516,8 +1620,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1569,14 +1677,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); } - const windowTop = this.__getFrameMetricsApprox( -@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ - borderColor: 'red', - borderWidth: 2, - }, -+ rowReverse: { -+ flexDirection: 'row-reverse', -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse', -+ }, - }); - export default VirtualizedList; -\ No newline at end of file + // If the user scrolls away from the start or end and back again, +@@ -1703,6 +1805,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1818,6 +1925,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1848,7 +1956,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1909,13 +2017,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1950,11 +2057,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -1995,6 +2099,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch deleted file mode 100644 index afd681bba3b0..000000000000 --- a/patches/react-native-web+0.19.9+002+fix-mvcp.patch +++ /dev/null @@ -1,687 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index a6fe142..faeb323 100644 ---- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] - - constructor(_props) { -- var _this$props$updateCel; -+ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; - super(_props); - this._getScrollMetrics = () => { - return this._scrollMetrics; -@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { - this._updateCellsToRender = () => { - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - this.setState((state, props) => { -- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); -+ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); - var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); - if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { - return null; -@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable - }; - }; -@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._getFrameMetrics = (index, props) => { - var data = props.data, -- getItem = props.getItem, - getItemCount = props.getItemCount, - getItemLayout = props.getItemLayout; - invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); -- var item = getItem(data, index); -- var frame = this._frames[this._keyExtractor(item, index, props)]; -+ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { - - // The last cell we rendered may be at a new index. Bail if we don't know - // where it is. -- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { -+ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { - return []; - } - var first = focusedCellIndex; -@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { - } - } - var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); -+ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; - this.state = { - cellsAroundViewport: initialRenderRegion, -- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) -+ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), -+ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { - var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; - var isEventTargetScrollable = scrollLength > clientLength; - var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; -- var leftoverDelta = delta; -+ var leftoverDelta = delta * 0.5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); - } -@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -+ static _findItemIndexWithKey(props, key, hint) { -+ var itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ var curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (var ii = 0; ii < itemCount; ii++) { -+ var _curKey = VirtualizedList._getItemKey(props, ii); -+ if (_curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ static _getItemKey(props, index) { -+ var item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } - static _createRenderMask(props, cellsAroundViewport, additionalRegions) { - var itemCount = props.getItemCount(props.data); - invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); -@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -- _adjustCellsAroundViewport(props, cellsAroundViewport) { -+ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { - var data = props.data, - getItemCount = props.getItemCount; - var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); -@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { - last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; - } - newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); -@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { - } - } - static getDerivedStateFromProps(newProps, prevState) { -+ var _newProps$maintainVis, _newProps$maintainVis2; - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - var itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } -- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); -+ var maintainVisibleContentPositionAdjustment = null; -+ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; -+ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; -+ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); -+ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { -+ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, -+ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment -+ } : prevState.cellsAroundViewport, newProps); - return { - cellsAroundViewport: constrainedCells, -- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) -+ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount - }; - } - _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { - last = Math.min(end, last); - var _loop = function _loop() { - var item = getItem(data, ii); -- var key = _this._keyExtractor(item, ii, _this.props); -+ var key = VirtualizedList._keyExtractor(item, ii, _this.props); - _this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - _this.pushOrUnshift(stickyHeaderIndices, cells.length); -@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { - } - static _constrainToItemCount(cells, props) { - var itemCount = props.getItemCount(props.data); -- var last = Math.min(itemCount - 1, cells.last); -+ var lastPossibleCellIndex = itemCount - 1; -+ -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); -+ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last) - }; - } - _isNestedWithSameOrientation() { - var nestedContext = this.context; - return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); - } -- _keyExtractor(item, index, props -- // $FlowFixMe[missing-local-annot] -- ) { -+ static _keyExtractor(item, index, props) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { - this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-header', - key: "$header" -- }, /*#__PURE__*/React.createElement(View, { -+ }, /*#__PURE__*/React.createElement(View -+ // We expect that header component will be a single native view so make it -+ // not collapsable to avoid this view being flattened and make this assumption -+ // no longer true. -+ , { -+ collapsable: false, - onLayout: this._onLayoutHeader, - style: [inversionStyle, this.props.ListHeaderComponentStyle] - }, -@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { - // TODO: Android support - invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, - stickyHeaderIndices, -- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style -+ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, -+ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) -+ }) : undefined - }); - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReached = _this$props8.onStartReached, - onStartReachedThreshold = _this$props8.onStartReachedThreshold, - onEndReached = _this$props8.onEndReached, -- onEndReachedThreshold = _this$props8.onEndReachedThreshold, -- initialScrollIndex = _this$props8.initialScrollIndex; -+ onEndReachedThreshold = _this$props8.onEndReachedThreshold; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - var _this$_scrollMetrics2 = this._scrollMetrics, - contentLength = _this$_scrollMetrics2.contentLength, - visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({ -- distanceFromStart -- }); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({ -+ distanceFromStart -+ }); - } - - // If the user scrolls away from the start or end and back again, -@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { - } - } - _updateViewableItems(props, cellsAroundViewport) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); - }); -diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index d896fb1..f303b31 100644 ---- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { - type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -+ // Used to track items added at the start of the list for maintainVisibleContentPosition. -+ firstVisibleItemKey: ?string, -+ // When > 0 the scroll position available in JS is considered stale and should not be used. -+ pendingScrollUpdateCount: number, - }; - - /** -@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); - -+ const minIndexForVisible = -+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), -+ firstVisibleItemKey: -+ this.props.getItemCount(this.props.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) -+ : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: -+ this.props.initialScrollIndex != null && -+ this.props.initialScrollIndex > 0 -+ ? 1 -+ : 0, - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { - const delta = this.props.horizontal - ? ev.deltaX || ev.wheelDeltaX - : ev.deltaY || ev.wheelDeltaY; -- let leftoverDelta = delta; -+ let leftoverDelta = delta * 5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 - ? Math.min(delta + scrollOffset, 0) -@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { - } - } - -+ static _findItemIndexWithKey( -+ props: Props, -+ key: string, -+ hint: ?number, -+ ): ?number { -+ const itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ const curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (let ii = 0; ii < itemCount; ii++) { -+ const curKey = VirtualizedList._getItemKey(props, ii); -+ if (curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ -+ static _getItemKey( -+ props: { -+ data: Props['data'], -+ getItem: Props['getItem'], -+ keyExtractor: Props['keyExtractor'], -+ ... -+ }, -+ index: number, -+ ): string { -+ const item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } -+ - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, -@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, -+ pendingScrollUpdateCount: number, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( -@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { - ), - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if ( -- props.initialScrollIndex && -- !this._scrollMetrics.offset && -- Math.abs(distanceFromEnd) >= Number.EPSILON -- ) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; -@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { - return prevState; - } - -+ let maintainVisibleContentPositionAdjustment: ?number = null; -+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ const minIndexForVisible = -+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ const newFirstVisibleItemKey = -+ newProps.getItemCount(newProps.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) -+ : null; -+ if ( -+ newProps.maintainVisibleContentPosition != null && -+ prevFirstVisibleItemKey != null && -+ newFirstVisibleItemKey != null -+ ) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ const hint = -+ itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( -+ newProps, -+ prevFirstVisibleItemKey, -+ hint, -+ ); -+ maintainVisibleContentPositionAdjustment = -+ firstVisibleItemIndex != null -+ ? firstVisibleItemIndex - minIndexForVisible -+ : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ - const constrainedCells = VirtualizedList._constrainToItemCount( -- prevState.cellsAroundViewport, -+ maintainVisibleContentPositionAdjustment != null -+ ? { -+ first: -+ prevState.cellsAroundViewport.first + -+ maintainVisibleContentPositionAdjustment, -+ last: -+ prevState.cellsAroundViewport.last + -+ maintainVisibleContentPositionAdjustment, -+ } -+ : prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: -+ maintainVisibleContentPositionAdjustment != null -+ ? prevState.pendingScrollUpdateCount + 1 -+ : prevState.pendingScrollUpdateCount, - }; - } - -@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { - - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); -- const key = this._keyExtractor(item, ii, this.props); -+ const key = VirtualizedList._keyExtractor(item, ii, this.props); - - this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); -+ this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - - const shouldListenForLayout = -@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); -- const last = Math.min(itemCount - 1, cells.last); -+ const lastPossibleCellIndex = itemCount - 1; - -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); -+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last, -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last), - }; - } - -@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; - -- _keyExtractor( -+ static _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, -- // $FlowFixMe[missing-local-annot] -- ) { -+ ): string { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { - cellKey={this._getCellKey() + '-header'} - key="$header"> - { - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, -+ maintainVisibleContentPosition: -+ this.props.maintainVisibleContentPosition != null -+ ? { -+ ...this.props.maintainVisibleContentPosition, -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: -+ this.props.maintainVisibleContentPosition.minIndexForVisible + -+ (this.props.ListHeaderComponent ? 1 : 0), -+ } -+ : undefined, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; -@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; - const inversionStyle = this.props.inverted -- ? this.props.horizontal -- ? styles.rowReverse -- : styles.columnReverse -- : null; -- -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, -- initialScrollIndex, - } = this.props; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; -@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({distanceFromStart}); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({distanceFromStart}); - } - - // If the user scrolls away from the start or end and back again, -@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale, - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, -+ state.pendingScrollUpdateCount, - ); - const renderMask = VirtualizedList._createRenderMask( - props, -@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable, - }; - }; -@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { - inLayout?: boolean, - ... - } => { -- const {data, getItem, getItemCount, getItemLayout} = props; -+ const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); -- const item = getItem(data, index); -- const frame = this._frames[this._keyExtractor(item, index, props)]; -+ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { - // where it is. - if ( - focusedCellIndex >= itemCount || -- this._keyExtractor( -- props.getItem(props.data, focusedCellIndex), -- focusedCellIndex, -- props, -- ) !== this._lastFocusedCellKey -+ VirtualizedList._getItemKey(props, focusedCellIndex) !== -+ this._lastFocusedCellKey - ) { - return []; - } -@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, diff --git a/patches/react-native-web+0.19.9+003+measureInWindow.patch b/patches/react-native-web+0.19.9+002+measureInWindow.patch similarity index 100% rename from patches/react-native-web+0.19.9+003+measureInWindow.patch rename to patches/react-native-web+0.19.9+002+measureInWindow.patch diff --git a/patches/react-native-web+0.19.9+004+fix-pointer-events.patch b/patches/react-native-web+0.19.9+003+fix-pointer-events.patch similarity index 100% rename from patches/react-native-web+0.19.9+004+fix-pointer-events.patch rename to patches/react-native-web+0.19.9+003+fix-pointer-events.patch diff --git a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch b/patches/react-native-web+0.19.9+005+fixLastSpacer.patch deleted file mode 100644 index 0ca5ac778e0b..000000000000 --- a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch +++ /dev/null @@ -1,29 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index faeb323..68d740a 100644 ---- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -78,14 +78,6 @@ function scrollEventThrottleOrDefault(scrollEventThrottle) { - function windowSizeOrDefault(windowSize) { - return windowSize !== null && windowSize !== void 0 ? windowSize : 21; - } --function findLastWhere(arr, predicate) { -- for (var i = arr.length - 1; i >= 0; i--) { -- if (predicate(arr[i])) { -- return arr[i]; -- } -- } -- return null; --} - - /** - * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) -@@ -1119,7 +1111,8 @@ class VirtualizedList extends StateSafePureComponent { - _keylessItemComponentName = ''; - var spacerKey = this._getSpacerKey(!horizontal); - var renderRegions = this.state.renderMask.enumerateRegions(); -- var lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); -+ var lastRegion = renderRegions[renderRegions.length - 1]; -+ var lastSpacer = lastRegion?.isSpacer ? lastRegion : null; - for (var _iterator = _createForOfIteratorHelperLoose(renderRegions), _step; !(_step = _iterator()).done;) { - var section = _step.value; - if (section.isSpacer) { \ No newline at end of file From 89227b2efe5b52bc559860b5182de6f913151f00 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 30 Dec 2023 15:35:08 +0100 Subject: [PATCH 037/124] fix Safari --- src/pages/home/report/ReportActionsView.js | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 1a28b222c4e9..383970a670d2 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -17,6 +17,7 @@ import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -98,7 +99,10 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { +// NOTE: The current delay is a temporary workaround due to a limitation in React Native Web. This will be removed once a forthcoming patch to React Native Web is applied. +const TIMEOUT = Browser.isSafari() && Browser.isMobileSafari ? 1100 : 70; + +const useHandleList = (linkedID, messageArray, fetchFn, route) => { const [edgeID, setEdgeID] = useState(linkedID); const [listID, setListID] = useState(() => Math.round(Math.random() * 100)); const isFirstRender = useRef(true); @@ -116,7 +120,6 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe setEdgeID(''); }, [route, linkedID]); - const cattedArray = useMemo(() => { if (!linkedID || index === -1) { return messageArray; @@ -130,7 +133,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe return messageArray.slice(newStartIndex, messageArray.length); } return messageArray; - }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); + }, [linkedID, messageArray, edgeID, index]); const hasMoreCashed = cattedArray.length < messageArray.length; @@ -143,10 +146,10 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe } if (isFirstRender.current) { - isFirstRender.current = false; - InteractionManager.runAfterInteractions(() => { + setTimeout(() => { + isFirstRender.current = false; setEdgeID(firstReportActionID); - }); + }, TIMEOUT); } else { setEdgeID(firstReportActionID); } @@ -172,7 +175,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const didSubscribeToReportTypingEvents = useRef(false); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); - const isInitial = useRef(true); const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); const {windowHeight} = useWindowDimensions(); @@ -327,14 +329,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; const SPACER = 30; const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; - - if ( - (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current && !isContentSmallerThanList) || - (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList) - ) { + if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { fetchFunc({firstReportActionID, distanceFromStart}); } - isInitial.current = false; }, [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], ); From ac7c0c4133fb31aefd789d767472d4fe813ee86a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 15:25:04 +0100 Subject: [PATCH 038/124] bump WINDOW_SIZE --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 783e88266803..45d5a996dead 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -2,7 +2,7 @@ import React, {ForwardedRef, forwardRef} from 'react'; import {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; -const WINDOW_SIZE = 15; +const WINDOW_SIZE = 21; function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( From 6fb63e469b17234a41d6c3a269475dc3b3f36b9b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 15:29:55 +0100 Subject: [PATCH 039/124] fix outdated loader data before navigating --- src/pages/home/ReportScreen.js | 38 ++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 219b42d8d06f..e8c399e52903 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -167,17 +167,21 @@ function ReportScreen({ const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); + const {reportActionID, reportID} = getReportActionID(route); - const firstRenderRef = useRef(true); const flatListRef = useRef(); const reactionListRef = useRef(); + const firstRenderRef = useRef(true); const prevReport = usePrevious(report); + const firstRenderLinkingLoaderRef = useRef(!!reportActionID); + const [firstRenderLinkingLoader, setFirstRenderLinkingLoader] = useState(!!reportActionID); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const {reportActionID, reportID} = getReportActionID(route); const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); const reportActions = useMemo(() => { - if (allReportActions?.length === 0) return []; + if (!!allReportActions && allReportActions.length === 0) { + return []; + } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); @@ -450,6 +454,32 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); + // Use `useMemo` to prevent displaying stale information. The `useMemo` hook is preferred over `useEffect` here because it runs during the render phase, thus avoiding a flash of outdated content which could occur if state updates were scheduled asynchronously. + // + // This `useMemo` handles the state just after initial report actions have been loaded. It ensures that the loader state is set correctly during the initial rendering phase when linking to a report. + useMemo(() => { + if (reportMetadata.isLoadingInitialReportActions) { + return; + } + requestAnimationFrame(() => { + firstRenderLinkingLoaderRef.current = true; + setFirstRenderLinkingLoader(true); + }); + }, [route, setFirstRenderLinkingLoader]); + // This `useMemo` updates the loader state after the initial rendering phase is complete and the report actions are no longer loading, ensuring the loader is hidden at the correct time. + useMemo(() => { + if (!firstRenderLinkingLoaderRef || !firstRenderLinkingLoaderRef.current || reportMetadata.isLoadingInitialReportActions) { + return; + } + requestAnimationFrame(() => { + firstRenderLinkingLoaderRef.current = false; + setFirstRenderLinkingLoader(false); + }); + }, [reportMetadata.isLoadingInitialReportActions, setFirstRenderLinkingLoader]); + const shouldShowSkeleton = useMemo( + () => firstRenderLinkingLoader || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), + [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions, firstRenderLinkingLoader], + ); return ( @@ -518,7 +548,7 @@ function ReportScreen({ {/* 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) && } + {shouldShowSkeleton && } {isReportReadyForDisplay ? ( Date: Wed, 3 Jan 2024 15:31:13 +0100 Subject: [PATCH 040/124] refactor initialNumToRender --- src/pages/home/report/ReportActionsList.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 16f2d95b23b0..9c1e592f6f2a 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -12,7 +12,6 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; @@ -493,8 +492,7 @@ function ReportActionsList({ renderItem={renderItem} contentContainerStyle={contentContainerStyle} keyExtractor={keyExtractor} - // initialNumToRender={initialNumToRender} - initialNumToRender={50} + initialNumToRender={initialNumToRender} onEndReached={loadOlderChats} onEndReachedThreshold={0.75} onStartReached={loadNewerChats} From 17a6b90e9fa852eb08a8203da587f0f9affd3301 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 16:18:10 +0100 Subject: [PATCH 041/124] add debounce for fetching newer actions --- src/pages/home/report/ReportActionsView.js | 40 ++++++++++++++-------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 383970a670d2..b9920724e582 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -99,8 +99,8 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -// NOTE: The current delay is a temporary workaround due to a limitation in React Native Web. This will be removed once a forthcoming patch to React Native Web is applied. -const TIMEOUT = Browser.isSafari() && Browser.isMobileSafari ? 1100 : 70; +// Set a longer timeout for Safari on mobile due to FlatList issues. +const TIMEOUT = Browser.isSafari() || Browser.isMobileSafari ? 200 : 100; const useHandleList = (linkedID, messageArray, fetchFn, route) => { const [edgeID, setEdgeID] = useState(linkedID); @@ -116,8 +116,11 @@ const useHandleList = (linkedID, messageArray, fetchFn, route) => { }, [messageArray, linkedID, edgeID]); useMemo(() => { - isFirstRender.current = true; - setEdgeID(''); + // Clear edgeID before navigating to a linked message + requestAnimationFrame(() => { + isFirstRender.current = true; + setEdgeID(''); + }); }, [route, linkedID]); const cattedArray = useMemo(() => { @@ -125,33 +128,42 @@ const useHandleList = (linkedID, messageArray, fetchFn, route) => { return messageArray; } if (isFirstRender.current) { + // On first render, position the view at the linked message setListID((i) => i + 1); return messageArray.slice(index, messageArray.length); } else if (edgeID) { - const amountOfItemsBeforeLinkedOne = 10; + // On subsequent renders, load additional messages + const amountOfItemsBeforeLinkedOne = 20; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - return messageArray.slice(newStartIndex, messageArray.length); + return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } return messageArray; }, [linkedID, messageArray, edgeID, index]); const hasMoreCashed = cattedArray.length < messageArray.length; + const debouncedSetEdgeID = _.throttle((firstReportActionID) => { + setEdgeID(firstReportActionID); + }, 200); + const paginate = useCallback( - ({firstReportActionID, distanceFromStart}) => { + ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { - fetchFn({distanceFromStart}); + // Fetch new messages if all current messages have been shown + fetchFn(); + setEdgeID(firstReportActionID); + return; } - if (isFirstRender.current) { + isFirstRender.current = false; + // Delay to ensure the linked message is displayed correctly. setTimeout(() => { - isFirstRender.current = false; setEdgeID(firstReportActionID); }, TIMEOUT); } else { - setEdgeID(firstReportActionID); + debouncedSetEdgeID(firstReportActionID); } }, [setEdgeID, fetchFn, hasMoreCashed], @@ -209,7 +221,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = _.last(reportActions); - const isWeReachedTheOldestAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; + const isWeReachedTheOldestAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; /** * @returns {Boolean} @@ -325,12 +337,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return - ({distanceFromStart}) => { + () => { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; const SPACER = 30; const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { - fetchFunc({firstReportActionID, distanceFromStart}); + fetchFunc({firstReportActionID}); } }, [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], From b9e739a060cc927f884933d01924d3e827493ecb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 18:18:51 +0100 Subject: [PATCH 042/124] add 'scroll to the bottom' --- src/pages/home/report/ReportActionsList.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 9c1e592f6f2a..867f767448c2 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -15,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; @@ -22,6 +23,7 @@ import reportPropTypes from '@pages/reportPropTypes'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import FloatingMessageCounter from './FloatingMessageCounter'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; @@ -167,6 +169,7 @@ function ReportActionsList({ const sortedVisibleReportActions = _.filter(sortedReportActions, (s) => isOffline || s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || s.errors); const lastActionIndex = lodashGet(sortedVisibleReportActions, [0, 'reportActionID']); const reportActionSize = useRef(sortedVisibleReportActions.length); + const hasNewestReportAction = lodashGet(sortedReportActions[0], 'created') === report.lastVisibleActionCreated; const previousLastIndex = useRef(lastActionIndex); @@ -319,6 +322,11 @@ function ReportActionsList({ }; const scrollToBottomAndMarkReportAsRead = () => { + if (!hasNewestReportAction) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); + Report.openReport({reportID: report.reportID}); + return; + } reportScrollManager.scrollToBottom(); readActionSkipped.current = false; Report.readNewestAction(report.reportID); @@ -457,7 +465,7 @@ function ReportActionsList({ ); const onContentSizeChangeInner = useCallback( (w, h) => { - onContentSizeChange(w,h) + onContentSizeChange(w, h); }, [onContentSizeChange], ); @@ -479,7 +487,7 @@ function ReportActionsList({ return ( <> From 2f7da440a805fef9cbf96f770d0c279c9d23af56 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 5 Jan 2024 15:17:11 +0100 Subject: [PATCH 043/124] refactor useMemo calculations --- src/components/FlatList/MVCPFlatList.js | 2 +- src/pages/home/ReportScreen.js | 30 +++++++++---------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 0abb1dc4a873..44cb50b98e11 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -46,7 +46,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; }, [horizontal]); - const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []); + const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode()?.childNodes[0], []); const scrollToOffset = React.useCallback( (offset, animated) => { diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index e8c399e52903..edcfa724c1d8 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,8 +1,8 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -173,7 +173,6 @@ function ReportScreen({ const reactionListRef = useRef(); const firstRenderRef = useRef(true); const prevReport = usePrevious(report); - const firstRenderLinkingLoaderRef = useRef(!!reportActionID); const [firstRenderLinkingLoader, setFirstRenderLinkingLoader] = useState(!!reportActionID); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); @@ -454,31 +453,24 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); - // Use `useMemo` to prevent displaying stale information. The `useMemo` hook is preferred over `useEffect` here because it runs during the render phase, thus avoiding a flash of outdated content which could occur if state updates were scheduled asynchronously. - // - // This `useMemo` handles the state just after initial report actions have been loaded. It ensures that the loader state is set correctly during the initial rendering phase when linking to a report. - useMemo(() => { - if (reportMetadata.isLoadingInitialReportActions) { + useLayoutEffect(() => { + if (!reportActionID) { return; } requestAnimationFrame(() => { - firstRenderLinkingLoaderRef.current = true; setFirstRenderLinkingLoader(true); }); - }, [route, setFirstRenderLinkingLoader]); - // This `useMemo` updates the loader state after the initial rendering phase is complete and the report actions are no longer loading, ensuring the loader is hidden at the correct time. - useMemo(() => { - if (!firstRenderLinkingLoaderRef || !firstRenderLinkingLoaderRef.current || reportMetadata.isLoadingInitialReportActions) { + }, [route, reportActionID]); + useEffect(() => { + if (!firstRenderLinkingLoader || reportMetadata.isLoadingInitialReportActions) { return; } - requestAnimationFrame(() => { - firstRenderLinkingLoaderRef.current = false; - setFirstRenderLinkingLoader(false); - }); - }, [reportMetadata.isLoadingInitialReportActions, setFirstRenderLinkingLoader]); + setFirstRenderLinkingLoader(false); + }, [firstRenderLinkingLoader, reportMetadata.isLoadingInitialReportActions]); + const shouldShowSkeleton = useMemo( () => firstRenderLinkingLoader || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions, firstRenderLinkingLoader], + [firstRenderLinkingLoader, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], ); return ( From 5a9bd2f1b101e42bde227179fcbeed6f2546821e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 6 Jan 2024 19:02:23 +0100 Subject: [PATCH 044/124] remove useMemo calculations --- src/components/FloatingActionButton.js | 5 +- src/pages/home/report/ReportActionsView.js | 86 +++++++++++----------- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index 59e741001063..bb973ab3665f 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -6,6 +6,7 @@ import Svg, {Path} from 'react-native-svg'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import CheckForPreviousReportActionIDClean from '@libs/migrations/CheckForPreviousReportActionIDClean'; import variables from '@styles/variables'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; @@ -106,7 +107,9 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility fabPressable.current.blur(); onPress(e); }} - onLongPress={() => {}} + onLongPress={() => { + CheckForPreviousReportActionIDClean(); + }} style={[styles.floatingActionButton, animatedStyle]} > { + const [edgeID, setEdgeID] = useState(); + const isCuttingForFirstRender = useRef(true); + + useLayoutEffect(() => { + setEdgeID(); + }, [route, linkedID]); + + const listID = useMemo(() => { + isCuttingForFirstRender.current = true; + listIDCount += 1; + return listIDCount; + }, [route]); -const useHandleList = (linkedID, messageArray, fetchFn, route) => { - const [edgeID, setEdgeID] = useState(linkedID); - const [listID, setListID] = useState(() => Math.round(Math.random() * 100)); - const isFirstRender = useRef(true); const index = useMemo(() => { if (!linkedID) { return -1; } - return messageArray.findIndex((obj) => String(obj.reportActionID) === String(edgeID || linkedID)); - }, [messageArray, linkedID, edgeID]); - - useMemo(() => { - // Clear edgeID before navigating to a linked message - requestAnimationFrame(() => { - isFirstRender.current = true; - setEdgeID(''); - }); - }, [route, linkedID]); + const indx = messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); + return indx; + }, [messageArray, edgeID, linkedID]); const cattedArray = useMemo(() => { - if (!linkedID || index === -1) { + if (!linkedID) { return messageArray; } - if (isFirstRender.current) { - // On first render, position the view at the linked message - setListID((i) => i + 1); + if (isLoading || index === -1) { + return []; + } + + if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); - } else if (edgeID) { - // On subsequent renders, load additional messages - const amountOfItemsBeforeLinkedOne = 20; + } else { + const amountOfItemsBeforeLinkedOne = 15; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } - return messageArray; - }, [linkedID, messageArray, edgeID, index]); + }, [linkedID, messageArray, index, isLoading, edgeID]); const hasMoreCashed = cattedArray.length < messageArray.length; - const debouncedSetEdgeID = _.throttle((firstReportActionID) => { - setEdgeID(firstReportActionID); - }, 200); - const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { - // Fetch new messages if all current messages have been shown + isCuttingForFirstRender.current = false; fetchFn(); - setEdgeID(firstReportActionID); - return; } - if (isFirstRender.current) { - isFirstRender.current = false; - // Delay to ensure the linked message is displayed correctly. - setTimeout(() => { + if (isCuttingForFirstRender.current) { + isCuttingForFirstRender.current = false; + InteractionManager.runAfterInteractions(() => { setEdgeID(firstReportActionID); - }, TIMEOUT); + }); } else { - debouncedSetEdgeID(firstReportActionID); + setEdgeID(firstReportActionID); } }, - [setEdgeID, fetchFn, hasMoreCashed], + [fetchFn, hasMoreCashed], ); return { @@ -216,8 +209,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], ); - const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route); - + const { + cattedArray: reportActions, + fetchFunc, + linkedIdIndex, + listID, + } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = _.last(reportActions); @@ -338,6 +335,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { + if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions) { + return; + } const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; const SPACER = 30; const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; From b3bd5d2fda8cd3148c61cb8a3239ee2a9bd80387 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 10:21:47 +0100 Subject: [PATCH 045/124] cleanup comments --- src/components/FloatingActionButton.js | 5 +-- src/libs/Permissions.ts | 2 +- src/libs/ReportActionsUtils.ts | 43 +------------------------- src/pages/home/ReportScreen.js | 9 +----- 4 files changed, 4 insertions(+), 55 deletions(-) diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index bb973ab3665f..59e741001063 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -6,7 +6,6 @@ import Svg, {Path} from 'react-native-svg'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import CheckForPreviousReportActionIDClean from '@libs/migrations/CheckForPreviousReportActionIDClean'; import variables from '@styles/variables'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; @@ -107,9 +106,7 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility fabPressable.current.blur(); onPress(e); }} - onLongPress={() => { - CheckForPreviousReportActionIDClean(); - }} + onLongPress={() => {}} style={[styles.floatingActionButton, animatedStyle]} > ): boolean { } function canUseCommentLinking(betas: OnyxEntry): boolean { - return '!!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas)'; + return !!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); } function canUseReportFields(betas: OnyxEntry): boolean { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index cb3e07afe692..853c871f1801 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -217,45 +217,6 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } -// /** -// * Given an object of reportActions, sorts them, and then adds the previousReportActionID to each item except the first. -// * @param {Object} reportActions -// * @returns {Array} -// */ -// function processReportActions(reportActions) { //TODO: remove after previousReportActionID is stable -// // Separate new and sorted reportActions -// const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); -// const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); - -// // Sort the new reportActions -// const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); - -// // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first -// const processedReportActions = sortedNewReportActions.map((action, index) => { -// if (index === sortedNewReportActions.length - 1) { -// return action; // Return the first item as is -// } -// return { -// ...action, -// previousReportActionID: sortedNewReportActions[index + 1].reportActionID, -// }; -// }); - -// if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { -// processedReportActions.pop(); -// } - -// // Determine the order of merging based on reportActionID values -// const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; -// const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; - -// if (firstProcessedReportActionID > lastSortedReportActionID) { -// return [...sortedReportActions, ...processedReportActions]; -// } else { -// return [...processedReportActions, ...sortedReportActions]; -// } -// } - /** * Returns the range of report actions from the given array which include current id * the range is consistent @@ -562,9 +523,7 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { - const filteredReportActions = Object.entries(reportActions ?? {}) - // .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) - .map((entry) => entry[1]); + const filteredReportActions = Object.entries(reportActions ?? {}).map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index edcfa724c1d8..65f7008b27d4 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -100,7 +100,6 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - // sortedReportActions: [], report: {}, reportMetadata: { isLoadingInitialReportActions: true, @@ -616,12 +615,6 @@ export default compose( key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, - // sortedReportActions: { - // key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - // canEvict: false, - // selector: ReportActionsUtils.getSortedReportActionsForDisplay, - // // selector: ReportActionsUtils.processReportActions, - // }, }, true, ), From edd217c91a6486e548d1e36c7f6511e1d02d51d4 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 18:21:03 +0100 Subject: [PATCH 046/124] fix setIsHovered warnings --- src/components/Hoverable/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index cbda0312beee..e8a39aea68a7 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -95,7 +95,7 @@ function Hoverable( } setIsHovered(hovered); }, - [disabled, shouldHandleScroll], + [disabled, shouldHandleScroll, setIsHovered], ); useEffect(() => { @@ -119,7 +119,7 @@ function Hoverable( }); return () => scrollingListener.remove(); - }, [shouldHandleScroll]); + }, [shouldHandleScroll, setIsHovered]); useEffect(() => { if (!DeviceCapabilities.hasHoverSupport()) { @@ -147,14 +147,14 @@ function Hoverable( document.addEventListener('mouseover', unsetHoveredIfOutside); return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); - }, [isHovered]); + }, [isHovered, setIsHovered]); useEffect(() => { if (!disabled || !isHovered) { return; } setIsHovered(false); - }, [disabled, isHovered]); + }, [disabled, isHovered, setIsHovered]); useEffect(() => { if (disabled) { @@ -209,7 +209,7 @@ function Hoverable( child.props.onBlur(event); } }, - [child.props], + [child.props, setIsHovered], ); // We need to access the ref of a children from both parent and current component From 68ee0ab9349b70f2f296fc93397bb0e133ace075 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 18:21:26 +0100 Subject: [PATCH 047/124] use patches --- ...eact-native+virtualized-lists+0.72.8.patch | 34 +++++++++++++++++++ ...-native-web+0.19.9+004+fixLastSpacer.patch | 29 ++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 patches/@react-native+virtualized-lists+0.72.8.patch create mode 100644 patches/react-native-web+0.19.9+004+fixLastSpacer.patch diff --git a/patches/@react-native+virtualized-lists+0.72.8.patch b/patches/@react-native+virtualized-lists+0.72.8.patch new file mode 100644 index 000000000000..b7f9c39f572d --- /dev/null +++ b/patches/@react-native+virtualized-lists+0.72.8.patch @@ -0,0 +1,34 @@ +diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +index ef5a3f0..2590edd 100644 +--- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js ++++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +@@ -125,19 +125,6 @@ function windowSizeOrDefault(windowSize: ?number) { + return windowSize ?? 21; + } + +-function findLastWhere( +- arr: $ReadOnlyArray, +- predicate: (element: T) => boolean, +-): T | null { +- for (let i = arr.length - 1; i >= 0; i--) { +- if (predicate(arr[i])) { +- return arr[i]; +- } +- } +- +- return null; +-} +- + /** + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) + * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better +@@ -1019,7 +1006,8 @@ class VirtualizedList extends StateSafePureComponent { + const spacerKey = this._getSpacerKey(!horizontal); + + const renderRegions = this.state.renderMask.enumerateRegions(); +- const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); ++ const lastRegion = renderRegions[renderRegions.length - 1]; ++ const lastSpacer = lastRegion?.isSpacer ? lastRegion : null; + + for (const section of renderRegions) { + if (section.isSpacer) { \ No newline at end of file diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch new file mode 100644 index 000000000000..f5441d087277 --- /dev/null +++ b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch @@ -0,0 +1,29 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index 7f6c880..b05da08 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -78,14 +78,6 @@ function scrollEventThrottleOrDefault(scrollEventThrottle) { + function windowSizeOrDefault(windowSize) { + return windowSize !== null && windowSize !== void 0 ? windowSize : 21; + } +-function findLastWhere(arr, predicate) { +- for (var i = arr.length - 1; i >= 0; i--) { +- if (predicate(arr[i])) { +- return arr[i]; +- } +- } +- return null; +-} + + /** + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) +@@ -1107,7 +1099,8 @@ class VirtualizedList extends StateSafePureComponent { + _keylessItemComponentName = ''; + var spacerKey = this._getSpacerKey(!horizontal); + var renderRegions = this.state.renderMask.enumerateRegions(); +- var lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); ++ var lastRegion = renderRegions[renderRegions.length - 1]; ++ var lastSpacer = lastRegion?.isSpacer ? lastRegion : null; + for (var _iterator = _createForOfIteratorHelperLoose(renderRegions), _step; !(_step = _iterator()).done;) { + var section = _step.value; + if (section.isSpacer) { From 7cdc05887c0e10478b1b5488e2a010bb3cc9bb0b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 18:52:02 +0100 Subject: [PATCH 048/124] optional scrollToBottom --- src/pages/home/report/ReportActionsList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 39423ed156e1..84e6a4a4d6c4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -262,6 +262,9 @@ function ReportActionsList({ }, [report.reportID]); useEffect(() => { + if (linkedReportActionID) { + return; + } InteractionManager.runAfterInteractions(() => { reportScrollManager.scrollToBottom(); }); From a8f17f8e473b6bbe1d9de928ae819ba34fc44725 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 19:48:35 +0100 Subject: [PATCH 049/124] hovering issue --- src/pages/home/report/ReportActionsView.js | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index cbe74e3fd551..782dcfa8acf7 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -97,10 +97,15 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } +const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; +const SPACER = 30; +const AMOUNT_OF_ITEMS_BEFORE_LINKED_ONE = 15; + let listIDCount = 1; const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { const [edgeID, setEdgeID] = useState(); const isCuttingForFirstRender = useRef(true); + const isCuttingForFirstBatch = useRef(false); useLayoutEffect(() => { setEdgeID(); @@ -108,18 +113,18 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { const listID = useMemo(() => { isCuttingForFirstRender.current = true; + isCuttingForFirstBatch.current = false; listIDCount += 1; return listIDCount; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); - const index = useMemo(() => { if (!linkedID) { return -1; } - const indx = messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); - return indx; + return messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); }, [messageArray, edgeID, linkedID]); const cattedArray = useMemo(() => { @@ -133,10 +138,13 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); } else { - const amountOfItemsBeforeLinkedOne = 15; - const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; + // Sometimes the layout is wrong. This helps get the slide right for one item. + const dynamicBatchSize = isCuttingForFirstBatch.current ? 1 : AMOUNT_OF_ITEMS_BEFORE_LINKED_ONE; + const newStartIndex = index >= dynamicBatchSize ? index - dynamicBatchSize : 0; + isCuttingForFirstBatch.current = false; return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); const hasMoreCashed = cattedArray.length < messageArray.length; @@ -151,6 +159,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { } if (isCuttingForFirstRender.current) { isCuttingForFirstRender.current = false; + isCuttingForFirstBatch.current = true; InteractionManager.runAfterInteractions(() => { setEdgeID(firstReportActionID); }); @@ -245,6 +254,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } Report.openReport({reportID, reportActionID}); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); useEffect(() => { @@ -307,6 +317,8 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro contentListHeight.current = h; }, []); + const checkIfContentSmallerThanList = useCallback(() => windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current, [windowHeight]); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. @@ -325,21 +337,28 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro Report.getOlderActions(reportID, oldestReportAction.reportActionID); }; - const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); + const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions) { return; } - const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; - const SPACER = 30; - const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; + const isContentSmallerThanList = checkIfContentSmallerThanList(); if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { fetchFunc({firstReportActionID}); } }, - [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], + [ + props.isLoadingInitialReportActions, + props.isLoadingOlderReportActions, + checkIfContentSmallerThanList, + reportActionID, + linkedIdIndex, + hasNewestReportAction, + fetchFunc, + firstReportActionID, + ], ); /** From e8dc64387e83b26a213785f9e51854f13f0c289f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 9 Jan 2024 12:32:20 +0100 Subject: [PATCH 050/124] fix loader blinking --- src/pages/home/ReportScreen.js | 38 ++++++++++++++++------------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 39f895fecc7e..268bcc2fa497 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -171,8 +171,8 @@ function ReportScreen({ const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); + const isLinkingLoaderRef = useRef(!!reportActionID); const prevReport = usePrevious(report); - const [firstRenderLinkingLoader, setFirstRenderLinkingLoader] = useState(!!reportActionID); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); @@ -184,8 +184,21 @@ function ReportScreen({ const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); return reportActionsWithoutDeleted; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportActionID, allReportActions, isOffline]); + + // We define this here because if we have a cached elements, reportActions would trigger them immediately, causing a visible blink. Therefore, it's necessary to define it simultaneously with reportActions. We use a ref for this purpose, as there's no need to trigger a re-render, unlike changing the state with isLoadingInitialReportActions would do. + useMemo(() => { + isLinkingLoaderRef.current = !!reportActionID; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route]); + useMemo(() => { + if (reportMetadata.isLoadingInitialReportActions) { + return; + } + isLinkingLoaderRef.current = false; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportMetadata.isLoadingInitialReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -453,24 +466,9 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); - useLayoutEffect(() => { - if (!reportActionID) { - return; - } - requestAnimationFrame(() => { - setFirstRenderLinkingLoader(true); - }); - }, [route, reportActionID]); - useEffect(() => { - if (!firstRenderLinkingLoader || reportMetadata.isLoadingInitialReportActions) { - return; - } - setFirstRenderLinkingLoader(false); - }, [firstRenderLinkingLoader, reportMetadata.isLoadingInitialReportActions]); - const shouldShowSkeleton = useMemo( - () => firstRenderLinkingLoader || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [firstRenderLinkingLoader, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], + () => isLinkingLoaderRef.current || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), + [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], ); return ( From 2ac370df9c1720cd795e0f3797ba6f80b394256e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 9 Jan 2024 18:29:16 +0100 Subject: [PATCH 051/124] temporary fix due to broken main --- src/components/Tooltip/BaseTooltip/index.tsx | 2 +- tests/actions/IOUTest.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index 2adde759b847..4e44f918a24a 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -189,7 +189,7 @@ function Tooltip( (e: MouseEvent) => { updateTargetAndMousePosition(e); if (React.isValidElement(children)) { - children.props.onMouseEnter(e); + // children.props.onMouseEnter(e); } }, [children, updateTargetAndMousePosition], diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 320f1203f4d2..c5dfd4909ce7 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -2259,7 +2259,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID From 5549ffd1d19c3e25f4f989e627c38c51e124889f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 21:40:56 +0100 Subject: [PATCH 052/124] implement scrolling functionality prior to adding pagination --- src/pages/home/ReportScreen.js | 46 ++++++++------- src/pages/home/report/ReportActionsList.js | 3 +- src/pages/home/report/ReportActionsView.js | 66 ++++++++++++++-------- 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 3345c6064aa5..312a02dfa1c2 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -173,10 +173,10 @@ function ReportScreen({ const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); - const isLinkingLoaderRef = useRef(!!reportActionID); + const shouldTriggerLoadingRef = useRef(!!reportActionID); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionID); const reportActions = useMemo(() => { if (!!allReportActions && allReportActions.length === 0) { @@ -189,18 +189,6 @@ function ReportScreen({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportActionID, allReportActions, isOffline]); - // We define this here because if we have a cached elements, reportActions would trigger them immediately, causing a visible blink. Therefore, it's necessary to define it simultaneously with reportActions. We use a ref for this purpose, as there's no need to trigger a re-render, unlike changing the state with isLoadingInitialReportActions would do. - useMemo(() => { - isLinkingLoaderRef.current = !!reportActionID; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route]); - useMemo(() => { - if (reportMetadata.isLoadingInitialReportActions) { - return; - } - isLinkingLoaderRef.current = false; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportMetadata.isLoadingInitialReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -211,6 +199,15 @@ function ReportScreen({ Performance.markStart(CONST.TIMING.CHAT_RENDER); } + // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. + useMemo(() => { + shouldTriggerLoadingRef.current = !!reportActionID; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, reportActionID]); + useLayoutEffect(() => { + setLinkingToMessage(!!reportActionID); + }, [route, reportActionID]); + const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -507,9 +504,22 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); const shouldShowSkeleton = useMemo( - () => isLinkingLoaderRef.current || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], + () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), + [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], ); + + // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. + useEffect(() => { + if (reportMetadata.isLoadingInitialReportActions && shouldTriggerLoadingRef.current) { + shouldTriggerLoadingRef.current = false; + return; + } + if (!reportMetadata.isLoadingInitialReportActions && !shouldTriggerLoadingRef.current) { + shouldTriggerLoadingRef.current = false; + setLinkingToMessage(false); + } + }, [reportMetadata.isLoadingInitialReportActions]); + return ( @@ -563,8 +573,6 @@ function ReportScreen({ { - const [edgeID, setEdgeID] = useState(); +let listIDCount = Math.round(Math.random() * 100); + +/** + * useHandleList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('cattedArray') based on the current linked message, + * and manages pagination through 'paginate' function. + * + * @param {string} linkedID - ID of the linked message used for initial focus. + * @param {array} messageArray - Array of messages. + * @param {function} fetchFn - Function to fetch more messages. + * @param {string} route - Current route, used to reset states on route change. + * @param {boolean} isLoading - Loading state indicator. + * @param {object} reportScrollManager - Manages scrolling functionality. + * @returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, reportScrollManager) => { + // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned + const [edgeID, setEdgeID] = useState(''); const isCuttingForFirstRender = useRef(true); - const isCuttingForFirstBatch = useRef(false); useLayoutEffect(() => { - setEdgeID(); + setEdgeID(''); }, [route, linkedID]); const listID = useMemo(() => { isCuttingForFirstRender.current = true; - isCuttingForFirstBatch.current = false; listIDCount += 1; return listIDCount; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -138,12 +153,10 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); } else { - // Sometimes the layout is wrong. This helps get the slide right for one item. - const dynamicBatchSize = isCuttingForFirstBatch.current ? 1 : AMOUNT_OF_ITEMS_BEFORE_LINKED_ONE; - const newStartIndex = index >= dynamicBatchSize ? index - dynamicBatchSize : 0; - isCuttingForFirstBatch.current = false; + const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } + // edgeID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); @@ -152,22 +165,19 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray - // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { isCuttingForFirstRender.current = false; fetchFn(); } if (isCuttingForFirstRender.current) { + // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. + // We manually trigger a scroll to a slight offset to ensure the expected scroll behavior. + reportScrollManager.ref.current?.scrollToOffset({animated: false, offset: 1}); isCuttingForFirstRender.current = false; - isCuttingForFirstBatch.current = true; - InteractionManager.runAfterInteractions(() => { - setEdgeID(firstReportActionID); - }); - } else { - setEdgeID(firstReportActionID); } + setEdgeID(firstReportActionID); }, - [fetchFn, hasMoreCashed], + [fetchFn, hasMoreCashed, reportScrollManager.ref], ); return { @@ -182,6 +192,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); + const reportScrollManager = useReportScrollManager(); const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); @@ -221,10 +232,11 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions); + } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); + const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); - const oldestReportAction = _.last(reportActions); + const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); const isWeReachedTheOldestAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; /** @@ -238,6 +250,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if (props.report.isOptimisticReport || !_.isEmpty(createChatError)) { return; } + Report.openReport({reportID, reportActionID}); }; @@ -253,6 +266,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if (!reportActionID) { return; } + Report.openReport({reportID, reportActionID}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); @@ -335,13 +349,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, reportID]); + }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, isWeReachedTheOldestAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { - if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions) { + if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { return; } const isContentSmallerThanList = checkIfContentSmallerThanList(); @@ -358,6 +372,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro hasNewestReportAction, fetchFunc, firstReportActionID, + props.network.isOffline, ], ); @@ -407,6 +422,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} + reportScrollManager={reportScrollManager} /> From 8036bce2a10f95ce27b72a92efbc9f8f49fc047d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 22:25:43 +0100 Subject: [PATCH 053/124] use memo for oldestReportAction after merge --- src/pages/home/report/ReportActionsView.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index c33e411f0ece..b52c7fbbbdf1 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -5,7 +5,6 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -343,8 +342,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - const oldestReportAction = _.last(props.reportActions); - // Don't load more chats if we're already at the beginning of the chat history if (!oldestReportAction || isWeReachedTheOldestAction) { return; From abe9dc43036aa33234facb0fa1d36705b2833007 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 23:15:34 +0100 Subject: [PATCH 054/124] scrollToOffsetWithoutAnimation --- src/components/Hoverable/ActiveHoverable.tsx | 4 ++-- src/hooks/useReportScrollManager/index.native.ts | 16 +++++++++++++++- src/hooks/useReportScrollManager/index.ts | 16 +++++++++++++++- src/hooks/useReportScrollManager/types.ts | 1 + src/pages/home/report/ReportActionsView.js | 4 ++-- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 8fff59fe6eba..6037092a562d 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -8,7 +8,7 @@ import type HoverableProps from './types'; type ActiveHoverableProps = Omit; type UseHoveredReturnType = [boolean, (newValue: boolean) => void]; - +// This is a workaround specifically for the web part of comment linking. Without this adjustment, you might observe sliding effects due to conflicts between MVCPFlatList implementation and this file. Check it once https://github.com/necolas/react-native-web/pull/2588 is merged function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): UseHoveredReturnType { const [state, setState] = useState(initialValue); @@ -20,7 +20,7 @@ function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): U } else { setState(newValue); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return [state, interceptedSetState]; } diff --git a/src/hooks/useReportScrollManager/index.native.ts b/src/hooks/useReportScrollManager/index.native.ts index 6666a4ebd0f2..0af995ddc1f0 100644 --- a/src/hooks/useReportScrollManager/index.native.ts +++ b/src/hooks/useReportScrollManager/index.native.ts @@ -29,7 +29,21 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current?.scrollToOffset({animated: false, offset: 0}); }, [flatListRef, setScrollPosition]); - return {ref: flatListRef, scrollToIndex, scrollToBottom}; + /** + * Scroll to the offset of the flatlist. + */ + const scrollToOffsetWithoutAnimation = useCallback( + (offset: number) => { + if (!flatListRef?.current) { + return; + } + + flatListRef.current.scrollToOffset({animated: false, offset}); + }, + [flatListRef], + ); + + return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts index 8b56cd639d08..d9b3605b9006 100644 --- a/src/hooks/useReportScrollManager/index.ts +++ b/src/hooks/useReportScrollManager/index.ts @@ -28,7 +28,21 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, [flatListRef]); - return {ref: flatListRef, scrollToIndex, scrollToBottom}; + /** + * Scroll to the bottom of the flatlist. + */ + const scrollToOffsetWithoutAnimation = useCallback( + (offset: number) => { + if (!flatListRef?.current) { + return; + } + + flatListRef.current.scrollToOffset({animated: false, offset}); + }, + [flatListRef], + ); + + return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/types.ts b/src/hooks/useReportScrollManager/types.ts index 5182f7269a9c..f29b5dfd44a2 100644 --- a/src/hooks/useReportScrollManager/types.ts +++ b/src/hooks/useReportScrollManager/types.ts @@ -4,6 +4,7 @@ type ReportScrollManagerData = { ref: FlatListRefType; scrollToIndex: (index: number, isEditing?: boolean) => void; scrollToBottom: () => void; + scrollToOffsetWithoutAnimation: (offset: number) => void; }; export default ReportScrollManagerData; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index b52c7fbbbdf1..8200f88b078c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -171,12 +171,12 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, report if (isCuttingForFirstRender.current) { // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. // We manually trigger a scroll to a slight offset to ensure the expected scroll behavior. - reportScrollManager.ref.current?.scrollToOffset({animated: false, offset: 1}); + reportScrollManager.scrollToOffsetWithoutAnimation(1); isCuttingForFirstRender.current = false; } setEdgeID(firstReportActionID); }, - [fetchFn, hasMoreCashed, reportScrollManager.ref], + [fetchFn, hasMoreCashed, reportScrollManager], ); return { From 498716727b0c23e185216dbb03b522347d1191eb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 23:23:04 +0100 Subject: [PATCH 055/124] undo runHoverAfterInteraction --- src/components/Hoverable/ActiveHoverable.tsx | 31 ++++-------------- .../CheckForPreviousReportActionIDClean.ts | 32 ------------------- 2 files changed, 7 insertions(+), 56 deletions(-) delete mode 100644 src/libs/migrations/CheckForPreviousReportActionIDClean.ts diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 6037092a562d..028fdd30cf35 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -1,32 +1,15 @@ import type {Ref} from 'react'; import {cloneElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter, InteractionManager} from 'react-native'; +import {DeviceEventEmitter} from 'react-native'; import mergeRefs from '@libs/mergeRefs'; import {getReturnValue} from '@libs/ValueUtils'; import CONST from '@src/CONST'; import type HoverableProps from './types'; type ActiveHoverableProps = Omit; -type UseHoveredReturnType = [boolean, (newValue: boolean) => void]; -// This is a workaround specifically for the web part of comment linking. Without this adjustment, you might observe sliding effects due to conflicts between MVCPFlatList implementation and this file. Check it once https://github.com/necolas/react-native-web/pull/2588 is merged -function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): UseHoveredReturnType { - const [state, setState] = useState(initialValue); - - const interceptedSetState = useCallback((newValue: boolean) => { - if (runHoverAfterInteraction) { - InteractionManager.runAfterInteractions(() => { - setState(newValue); - }); - } else { - setState(newValue); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return [state, interceptedSetState]; -} -function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, runHoverAfterInteraction = false}: ActiveHoverableProps, outerRef: Ref) { - const [isHovered, setIsHovered] = useHovered(false, runHoverAfterInteraction); +function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: ActiveHoverableProps, outerRef: Ref) { + const [isHovered, setIsHovered] = useState(false); const elementRef = useRef(null); const isScrollingRef = useRef(false); @@ -40,7 +23,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r } setIsHovered(hovered); }, - [setIsHovered, shouldHandleScroll], + [shouldHandleScroll], ); useEffect(() => { @@ -64,7 +47,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r }); return () => scrollingListener.remove(); - }, [setIsHovered, shouldHandleScroll]); + }, [shouldHandleScroll]); useEffect(() => { // Do not mount a listener if the component is not hovered @@ -89,7 +72,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r document.addEventListener('mouseover', unsetHoveredIfOutside); return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); - }, [setIsHovered, isHovered, elementRef]); + }, [isHovered, elementRef]); useEffect(() => { const unsetHoveredWhenDocumentIsHidden = () => document.visibilityState === 'hidden' && setIsHovered(false); @@ -130,7 +113,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r child.props.onBlur?.(event); }, - [setIsHovered, child.props], + [child.props], ); return cloneElement(child, { diff --git a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts deleted file mode 100644 index 4362ae79114b..000000000000 --- a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Onyx, {OnyxCollection} from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as OnyxTypes from '@src/types/onyx'; - -function getReportActionsFromOnyx(): Promise> { - return new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - return resolve(allReportActions); - }, - }); - }); -} - -/** - * This migration checks for the 'previousReportActionID' key in the first valid reportAction of a report in Onyx. - * If the key is not found then all reportActions for all reports are removed from Onyx. - */ -export default function (): Promise { - return getReportActionsFromOnyx().then((allReportActions) => { - const onyxData: OnyxCollection = {}; - - Object.keys(allReportActions ?? {}).forEach((onyxKey) => { - onyxData[onyxKey] = {}; - }); - - return Onyx.multiSet(onyxData as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, Record>); - }); -} From 7aa008a1e9c0bc1223bc64d3361144f4b81b858e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 23:46:17 +0100 Subject: [PATCH 056/124] remove outdated test --- tests/unit/ReportActionsUtilsTest.js | 59 ---------------------------- 1 file changed, 59 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index efdfc7ba10c4..107941e32006 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -191,65 +191,6 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); - describe('getSortedReportActionsForDisplay with marked the first reportAction', () => { - it('should filter out non-whitelisted actions', () => { - const input = [ - { - created: '2022-11-13 22:27:01.825', - reportActionID: '8401445780099176', - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-12 22:27:01.825', - reportActionID: '6401435781022176', - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-11 22:27:01.825', - reportActionID: '2962390724708756', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-10 22:27:01.825', - reportActionID: '1609646094152486', - actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-09 22:27:01.825', - reportActionID: '8049485084562457', - actionName: CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.UPDATE_FIELD, - message: [{html: 'updated the Approval Mode from "Submit and Approve" to "Submit and Close"'}], - }, - { - created: '2022-11-08 22:27:06.825', - reportActionID: '1661970171066216', - actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED, - message: [{html: 'Waiting for the bank account'}], - }, - { - created: '2022-11-06 22:27:08.825', - reportActionID: '1661970171066220', - actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, - message: [{html: 'I have changed the task'}], - }, - ]; - - const resultWithoutNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input); - const resultWithNewestFlag = ReportActionsUtils.getReportActionsWithoutRemoved(input, true); - input.pop(); - // Mark the newest report action as the newest report action - resultWithoutNewestFlag[0] = { - ...resultWithoutNewestFlag[0], - isNewestReportAction: true, - }; - expect(resultWithoutNewestFlag).toStrictEqual(resultWithNewestFlag); - }); - }); - it('should filter out closed actions', () => { const input = [ { From dda07b4371c28bae3c844c23c1b02fab333109f5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 12:24:00 +0100 Subject: [PATCH 057/124] rename const --- .../workflows/reassurePerformanceTests.yml | 1 - src/components/Hoverable/types.ts | 3 - src/libs/ReportActionsUtils.ts | 33 +++++------ src/pages/home/ReportScreen.js | 57 +++++++------------ 4 files changed, 35 insertions(+), 59 deletions(-) diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index 116f178868c1..64b4536d9241 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -42,4 +42,3 @@ jobs: with: DURATION_DEVIATION_PERCENTAGE: 20 COUNT_DEVIATION: 0 - diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 13059c2e8316..6963e3b5178c 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -18,9 +18,6 @@ type HoverableProps = { /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ shouldHandleScroll?: boolean; - - /** Call setHovered(true) with runAfterInteraction */ - runHoverAfterInteraction?: boolean; }; export default HoverableProps; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8ab68e6e278e..511c1e782864 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -216,25 +216,19 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } - -/** - * Returns the range of report actions from the given array which include current id - * the range is consistent - * - * param {ReportAction[]} array - * param {String} id - * returns {ReportAction} - */ -function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction[] { +// Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. +// See unit tests for example of inputs and expected outputs. +function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; if (id) { - index = array.findIndex((obj) => obj.reportActionID === id); + index = sortedReportActions.findIndex((obj) => obj.reportActionID === id); } else { - index = array.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + index = sortedReportActions.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); } if (index === -1) { + Log.hmmm('[getContinuousReportActionChain] The linked reportAction is missing and needs to be fetched'); return []; } @@ -244,23 +238,23 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction // Iterate forwards through the array, starting from endIndex. This loop checks the continuity of actions by: // 1. Comparing the current item's previousReportActionID with the next item's reportActionID. // This ensures that we are moving in a sequence of related actions from newer to older. - while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { + while (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) { endIndex++; } - // Iterate backwards through the array, starting from startIndex. This loop has two main checks: + // Iterate backwards through the sortedReportActions, starting from startIndex. This loop has two main checks: // 1. It compares the current item's reportActionID with the previous item's previousReportActionID. // This is to ensure continuity in a sequence of actions. // 2. If the first condition fails, it then checks if the previous item has a pendingAction of 'add'. // This additional check is to include recently sent messages that might not yet be part of the established sequence. while ( - (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) || - array[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD + (startIndex > 0 && sortedReportActions[startIndex].reportActionID === sortedReportActions[startIndex - 1].previousReportActionID) || + sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD ) { startIndex--; } - return array.slice(startIndex, endIndex + 1); + return sortedReportActions.slice(startIndex, endIndex + 1); } /** @@ -533,7 +527,8 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { - const filteredReportActions = Object.entries(reportActions ?? {}).map((entry) => entry[1]); + const filteredReportActions = Object.values(reportActions ?? {}); + const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); } @@ -897,7 +892,7 @@ export { shouldReportActionBeVisible, shouldHideNewMarker, shouldReportActionBeVisibleAsLastAction, - getRangeFromArrayByID, + getContinuousReportActionChain, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, isMemberChangeAction, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 312a02dfa1c2..fd9872cd4d65 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -134,17 +134,6 @@ function getReportID(route) { // Placing the default value outside of `lodash.get()` is intentional. return String(lodashGet(route, 'params.reportID') || 0); } -/** - * Get the currently viewed report ID as number - * - * @param {Object} route - * @param {Object} route.params - * @param {String} route.params.reportID - * @returns {String} - */ -function getReportActionID(route) { - return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; -} function ReportScreen({ betas, @@ -168,26 +157,27 @@ function ReportScreen({ const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); - const {reportActionID, reportID} = getReportActionID(route); - const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); - const shouldTriggerLoadingRef = useRef(!!reportActionID); + const reportIDFromRoute = getReportID(route); + const reportActionIDFromRoute = lodashGet(route, 'params.reportActionID', null); + const shouldTriggerLoadingRef = useRef(!!reportActionIDFromRoute); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionID); + const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { if (!!allReportActions && allReportActions.length === 0) { return []; } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); - const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); + const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); + // eslint-disable-next-line rulesdir/prefer-underscore-method + const reportActionsWithoutDeleted = currentRangeOfReportActions.filter((item) => ReportActionsUtils.shouldReportActionBeVisible(item, item.reportActionID)); return reportActionsWithoutDeleted; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportActionID, allReportActions, isOffline]); + }, [reportActionIDFromRoute, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); @@ -200,13 +190,10 @@ function ReportScreen({ } // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. - useMemo(() => { - shouldTriggerLoadingRef.current = !!reportActionID; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, reportActionID]); useLayoutEffect(() => { - setLinkingToMessage(!!reportActionID); - }, [route, reportActionID]); + shouldTriggerLoadingRef.current = !!reportActionIDFromRoute; + setLinkingToMessage(!!reportActionIDFromRoute); + }, [route, reportActionIDFromRoute]); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -218,7 +205,7 @@ function ReportScreen({ const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); - const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails); + const isLoading = !reportIDFromRoute || !isSidebarLoaded || _.isEmpty(personalDetails); const parentReportAction = ReportActionsUtils.getParentReportAction(report); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); @@ -241,7 +228,7 @@ function ReportScreen({ let headerView = ( { - Report.openReport({reportID, reportActionID: reportActionID || ''}); - }, [reportID, reportActionID]); + Report.openReport({reportID: reportIDFromRoute, reportActionID: reportActionIDFromRoute || ''}); + }, [reportIDFromRoute, reportActionIDFromRoute]); const isFocused = useIsFocused(); useEffect(() => { @@ -463,7 +450,7 @@ function ReportScreen({ ]); useEffect(() => { - if (!ReportUtils.isValidReportIDFromPath(reportID)) { + if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { return; } // Ensures subscription event succeeds when the report/workspace room is created optimistically. @@ -472,10 +459,10 @@ function ReportScreen({ // Existing reports created will have empty fields for `pendingFields`. const didCreateReportSuccessfully = !report.pendingFields || (!report.pendingFields.addWorkspaceRoom && !report.pendingFields.createChat); if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { - Report.subscribeToReportLeavingEvents(reportID); + Report.subscribeToReportLeavingEvents(reportIDFromRoute); didSubscribeToReportLeavingEvents.current = true; } - }, [report, didSubscribeToReportLeavingEvents, reportID]); + }, [report, didSubscribeToReportLeavingEvents, reportIDFromRoute]); const onListLayout = useCallback((e) => { setListHeight((prev) => lodashGet(e, 'nativeEvent.layout.height', prev)); @@ -504,8 +491,8 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); const shouldShowSkeleton = useMemo( - () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], + () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions), + [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionIDFromRoute, reportMetadata.isLoadingInitialReportActions], ); // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. @@ -515,7 +502,6 @@ function ReportScreen({ return; } if (!reportMetadata.isLoadingInitialReportActions && !shouldTriggerLoadingRef.current) { - shouldTriggerLoadingRef.current = false; setLinkingToMessage(false); } }, [reportMetadata.isLoadingInitialReportActions]); @@ -574,7 +560,7 @@ function ReportScreen({ reportActions={reportActions} report={report} fetchReport={fetchReport} - reportActionID={reportActionID} + reportActionID={reportActionIDFromRoute} isLoadingInitialReportActions={reportMetadata.isLoadingInitialReportActions} isLoadingNewerReportActions={reportMetadata.isLoadingNewerReportActions} isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} @@ -626,7 +612,6 @@ export default compose( allReportActions: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, - selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, From 4f9d65eacfa1395bd79908b948a0bed76cbe32e5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 15:09:13 +0100 Subject: [PATCH 058/124] remove isLoadingInitialReportActions --- src/pages/home/ReportScreen.js | 14 +++++--------- src/pages/home/report/ReportActionsView.js | 17 +---------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index fd9872cd4d65..a49e6dda88de 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -165,7 +165,7 @@ function ReportScreen({ const shouldTriggerLoadingRef = useRef(!!reportActionIDFromRoute); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); + const [isPrepareLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { if (!!allReportActions && allReportActions.length === 0) { @@ -197,10 +197,6 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); 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) && reportMetadata.isLoadingInitialReportActions; - const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); @@ -296,12 +292,12 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route) && !isLoadingInitialReportActions) { + if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { return; } fetchReport(); - }, [report.reportID, route, isLoadingInitialReportActions, fetchReport]); + }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions, fetchReport]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -491,8 +487,8 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); const shouldShowSkeleton = useMemo( - () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions), - [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionIDFromRoute, reportMetadata.isLoadingInitialReportActions], + () => isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoading || reportMetadata.isLoadingInitialReportActions, + [isPrepareLinkingToMessage, isReportReadyForDisplay, isLoading, reportMetadata.isLoadingInitialReportActions], ); // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8200f88b078c..e3090e24f0c8 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,6 +1,3 @@ -/* eslint-disable no-else-return */ - -/* eslint-disable rulesdir/prefer-underscore-method */ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -85,18 +82,6 @@ const defaultProps = { }, }; -/** - * Get the currently viewed report ID as number - * - * @param {Object} route - * @param {Object} route.params - * @param {String} route.params.reportID - * @returns {String} - */ -function getReportActionID(route) { - return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; -} - const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; const PAGINATION_SIZE = 15; @@ -192,7 +177,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const reportScrollManager = useReportScrollManager(); - const {reportActionID} = getReportActionID(route); + const reportActionID = lodashGet(route, 'params.reportActionID', null); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const contentListHeight = useRef(0); From f459394cf5fbc81c18cbca9758ef81ae8ee67568 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 16:13:29 +0100 Subject: [PATCH 059/124] refactor getSortedReportActionsForDisplay --- patches/@react-native+virtualized-lists+0.72.8.patch | 2 +- src/libs/ReportActionsUtils.ts | 12 ++++++++++-- src/pages/home/ReportScreen.js | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/patches/@react-native+virtualized-lists+0.72.8.patch b/patches/@react-native+virtualized-lists+0.72.8.patch index b7f9c39f572d..a3bef95f1618 100644 --- a/patches/@react-native+virtualized-lists+0.72.8.patch +++ b/patches/@react-native+virtualized-lists+0.72.8.patch @@ -31,4 +31,4 @@ index ef5a3f0..2590edd 100644 + const lastSpacer = lastRegion?.isSpacer ? lastRegion : null; for (const section of renderRegions) { - if (section.isSpacer) { \ No newline at end of file + if (section.isSpacer) { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 511c1e782864..a3a051968516 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -526,8 +526,16 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { - const filteredReportActions = Object.values(reportActions ?? {}); +function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldIncludeInvisibleActions = true): ReportAction[] { + let filteredReportActions; + + if (shouldIncludeInvisibleActions) { + filteredReportActions = Object.entries(reportActions ?? {}) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) + .map((entry) => entry[1]); + } else { + filteredReportActions = Object.values(reportActions ?? {}); + } const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a49e6dda88de..65c2c6bfc25b 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -171,7 +171,7 @@ function ReportScreen({ if (!!allReportActions && allReportActions.length === 0) { return []; } - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, false); const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); // eslint-disable-next-line rulesdir/prefer-underscore-method const reportActionsWithoutDeleted = currentRangeOfReportActions.filter((item) => ReportActionsUtils.shouldReportActionBeVisible(item, item.reportActionID)); From 8be49c4f14bbe7aafcda9a8e5dde3a99ca3ab901 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 16:33:45 +0100 Subject: [PATCH 060/124] refactor openReport action --- .../ReportActionItem/MoneyRequestAction.js | 4 +-- src/libs/actions/Report.ts | 25 ++++++++----------- src/pages/home/ReportScreen.js | 2 +- .../report/ContextMenu/ContextMenuActions.js | 4 +-- src/pages/home/report/ReportActionsList.js | 2 +- src/pages/home/report/ReportActionsView.js | 4 +-- .../withReportAndReportActionOrNotFound.tsx | 2 +- tests/actions/IOUTest.js | 10 ++++---- tests/actions/ReportTest.js | 2 +- 9 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index f988542a3e6c..35e8fd3dcd68 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -108,11 +108,11 @@ function MoneyRequestAction({ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(action, requestReportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, action.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, action.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport({reportID: childReportID}); + Report.openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index dd783dde5037..f56fea47f09b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -460,16 +460,12 @@ function reportActionsExist(reportID: string): boolean { return allReportActions?.[reportID] !== undefined; } -type OpenReportProps = { - reportID: string; - reportActionID?: string; -}; - /** * Gets the latest page of report actions and updates the last read message * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * - * @param Object reportID, reportActionID + * @param reportID The ID of the report to open + * @param reportActionID The ID of the report action to navigate to * @param participantLoginList The list of users that are included in a new chat, not including the user creating it * @param newReportObject The optimistic report object created when making a new chat, saved as optimistic data * @param parentReportActionID The parent report action that a thread was created from (only passed for new threads) @@ -477,7 +473,8 @@ type OpenReportProps = { * @param participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it */ function openReport( - {reportID, reportActionID}: OpenReportProps, + reportID: string, + reportActionID?: string, participantLoginList: string[] = [], newReportObject: Partial = {}, parentReportActionID = '0', @@ -697,7 +694,7 @@ function navigateToAndOpenReport(userLogins: string[], shouldDismissModal = true const reportID = chat ? chat.reportID : newChat.reportID; // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server - openReport({reportID}, userLogins, newChat); + openReport(reportID, '', userLogins, newChat); if (shouldDismissModal) { Navigation.dismissModal(reportID); } else { @@ -719,7 +716,7 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) const reportID = chat ? chat.reportID : newChat.reportID; // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server - openReport({reportID}, [], newChat, '0', false, participantAccountIDs); + openReport(reportID, '', [], newChat, '0', false, participantAccountIDs); Navigation.dismissModal(reportID); } @@ -732,7 +729,7 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) */ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0') { if (childReportID !== '0') { - openReport({reportID: childReportID}); + openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); } else { const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])]; @@ -753,7 +750,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: P ); const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat?.participantAccountIDs ?? []); - openReport({reportID: newChat.reportID}, participantLogins, newChat, parentReportAction.reportActionID); + openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newChat.reportID)); } } @@ -1456,7 +1453,7 @@ function updateNotificationPreference( */ function toggleSubscribeToChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0', prevNotificationPreference?: NotificationPreference) { if (childReportID !== '0') { - openReport({reportID: childReportID}); + openReport(childReportID); const parentReportActionID = parentReportAction?.reportActionID ?? '0'; if (!prevNotificationPreference || prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false, parentReportID, parentReportActionID); @@ -1482,7 +1479,7 @@ function toggleSubscribeToChildReport(childReportID = '0', parentReportAction: P ); const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); - openReport({reportID: newChat.reportID}, participantLogins, newChat, parentReportAction.reportActionID); + openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID); const notificationPreference = prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; updateNotificationPreference(newChat.reportID, prevNotificationPreference, notificationPreference, false, parentReportID, parentReportAction?.reportActionID); @@ -2030,7 +2027,7 @@ function openReportFromDeepLink(url: string, isAuthenticated: boolean) { if (reportID && !isAuthenticated) { // Call the OpenReport command to check in the server if it's a public room. If so, we'll open it as an anonymous user - openReport({reportID}, [], {}, '0', true); + openReport(reportID, '', [], {}, '0', true); // Show the sign-in page if the app is offline if (isNetworkOffline) { diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 65c2c6bfc25b..edbf153cee24 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -269,7 +269,7 @@ function ReportScreen({ }, [route, report]); const fetchReport = useCallback(() => { - Report.openReport({reportID: reportIDFromRoute, reportActionID: reportActionIDFromRoute || ''}); + Report.openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); const isFocused = useIsFocused(); diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index ae62ce067b80..0fb6b5bba412 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -376,11 +376,11 @@ export default [ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, reportAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, reportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport({reportID: childReportID}); + Report.openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index bff5024e6bff..e5138a1d8c63 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -343,7 +343,7 @@ function ReportActionsList({ const scrollToBottomAndMarkReportAsRead = () => { if (!hasNewestReportAction) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); - Report.openReport({reportID: report.reportID}); + Report.openReport(report.reportID); return; } reportScrollManager.scrollToBottom(); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e3090e24f0c8..a181240e298f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -235,7 +235,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - Report.openReport({reportID, reportActionID}); + Report.openReport(reportID, reportActionID); }; useEffect(() => { @@ -251,7 +251,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - Report.openReport({reportID, reportActionID}); + Report.openReport(reportID, reportActionID); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 1ec956a2b09c..fb0a00e2d10d 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -63,7 +63,7 @@ export default function (WrappedComponent: if (!props.isSmallScreenWidth || (isNotEmptyObject(props.report) && isNotEmptyObject(reportAction))) { return; } - Report.openReport({reportID: props.route.params.reportID}); + Report.openReport(props.route.params.reportID); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.isSmallScreenWidth, props.route.params.reportID]); diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 7dbbc05f95f5..b0b44ea204d7 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -2183,7 +2183,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID @@ -2262,7 +2262,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID @@ -2332,7 +2332,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); jest.advanceTimersByTime(10); - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); Onyx.connect({ @@ -2424,7 +2424,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); @@ -2650,7 +2650,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); const allReportActions = await new Promise((resolve) => { diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index d118fd3a977e..a94db507637b 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -268,7 +268,7 @@ describe('actions/Report', () => { // When the user visits the report jest.advanceTimersByTime(10); currentTime = DateUtils.getDBTime(); - Report.openReport({reportID: REPORT_ID}); + Report.openReport(REPORT_ID); Report.readNewestAction(REPORT_ID); waitForBatchedUpdates(); return waitForBatchedUpdates(); From d67396fee59bbc97bf01f27cf98d99660d184c9f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 17:04:43 +0100 Subject: [PATCH 061/124] refactor initialNumToRender --- src/pages/home/ReportScreen.js | 7 ++++++- .../home/report/getInitialNumToRender/index.native.ts | 4 ++++ src/pages/home/report/getInitialNumToRender/index.ts | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/pages/home/report/getInitialNumToRender/index.native.ts create mode 100644 src/pages/home/report/getInitialNumToRender/index.ts diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index edbf153cee24..c6e13c2fc572 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -268,6 +268,11 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); + const isShowReportActionList = useMemo( + () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), + [isReportReadyForDisplay, isLoading, reportActions, reportMetadata.isLoadingInitialReportActions], + ); + const fetchReport = useCallback(() => { Report.openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); @@ -551,7 +556,7 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && ( + {isShowReportActionList && ( Date: Thu, 11 Jan 2024 18:38:17 +0100 Subject: [PATCH 062/124] replace getReportID in ReportScreen --- src/pages/home/ReportScreen.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 11d1505a1e8d..90bb3d80df74 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -208,7 +208,7 @@ function ReportScreen({ const isLoading = !reportIDFromRoute || !isSidebarLoaded || _.isEmpty(personalDetails); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; - const isTopMostReportId = currentReportID === getReportID(route); + const isTopMostReportId = currentReportID === reportIDFromRoute; const didSubscribeToReportLeavingEvents = useRef(false); useEffect(() => { @@ -260,12 +260,10 @@ function ReportScreen({ * @returns {Boolean} */ const isReportReadyForDisplay = useMemo(() => { - const reportIDFromPath = getReportID(route); - // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely - const isTransitioning = report && report.reportID !== reportIDFromPath; - return reportIDFromPath !== '' && report.reportID && !isTransitioning; - }, [route, report]); + const isTransitioning = report && report.reportID !== reportIDFromRoute; + return reportIDFromRoute !== '' && report.reportID && !isTransitioning; + }, [report, reportIDFromRoute]); const isShowReportActionList = useMemo( () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), @@ -285,23 +283,21 @@ function ReportScreen({ }, [report.reportID, isFocused]); const fetchReportIfNeeded = useCallback(() => { - const reportIDFromPath = getReportID(route); - // Report ID will be empty when the reports collection is empty. // This could happen when we are loading the collection for the first time after logging in. - if (!ReportUtils.isValidReportIDFromPath(reportIDFromPath)) { + if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { return; } // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { + if (report.reportID && report.reportID === reportIDFromRoute && !reportMetadata.isLoadingInitialReportActions) { return; } fetchReport(); - }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions, fetchReport]); + }, [report.reportID, reportMetadata.isLoadingInitialReportActions, fetchReport, reportIDFromRoute]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -339,10 +335,10 @@ function ReportScreen({ if (email) { assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; } - Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); + Task.createTaskAndNavigate(reportIDFromRoute, title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); return true; }, - [allPersonalDetails, report.policyID, route], + [allPersonalDetails, report.policyID, reportIDFromRoute], ); /** @@ -354,9 +350,9 @@ function ReportScreen({ if (isTaskCreated) { return; } - Report.addComment(getReportID(route), text); + Report.addComment(reportIDFromRoute, text); }, - [route, handleCreateTask], + [handleCreateTask, reportIDFromRoute], ); // Clear notifications for the current report when it's opened and re-focused @@ -398,7 +394,6 @@ function ReportScreen({ const onyxReportID = report.reportID; const prevOnyxReportID = prevReport.reportID; - const routeReportID = getReportID(route); // Navigate to the Concierge chat if the room was removed from another device (e.g. user leaving a room or removed from a room) if ( @@ -406,7 +401,7 @@ function ReportScreen({ (!prevUserLeavingStatus && userLeavingStatus) || // optimistic case (prevOnyxReportID && - prevOnyxReportID === routeReportID && + prevOnyxReportID === reportIDFromRoute && !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS.OPEN && (report.statusNum === CONST.REPORT.STATUS.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || @@ -429,7 +424,7 @@ function ReportScreen({ // the ReportScreen never actually unmounts and the reportID in the route also doesn't change. // Therefore, we need to compare if the existing reportID is the same as the one in the route // before deciding that we shouldn't call OpenReport. - if (onyxReportID === prevReport.reportID && (!onyxReportID || onyxReportID === routeReportID)) { + if (onyxReportID === prevReport.reportID && (!onyxReportID || onyxReportID === reportIDFromRoute)) { return; } @@ -447,6 +442,7 @@ function ReportScreen({ prevReport.parentReportID, prevReport.chatType, prevReport, + reportIDFromRoute, ]); useEffect(() => { From d6b6c1123a1f3125dcfa14341c65b485ed84427c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 21:39:58 +0100 Subject: [PATCH 063/124] renaming --- src/libs/ReportActionsUtils.ts | 8 +++---- src/pages/home/report/ReportActionItem.js | 1 - src/pages/home/report/ReportActionsView.js | 27 +++++++++++----------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index a3a051968516..838c6e4f646f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -218,6 +218,7 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort } // Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. // See unit tests for example of inputs and expected outputs. +// Note: sortedReportActions sorted in descending order function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; @@ -228,7 +229,6 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? } if (index === -1) { - Log.hmmm('[getContinuousReportActionChain] The linked reportAction is missing and needs to be fetched'); return []; } @@ -526,15 +526,15 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldIncludeInvisibleActions = true): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldIncludeInvisibleActions = false): ReportAction[] { let filteredReportActions; if (shouldIncludeInvisibleActions) { + filteredReportActions = Object.values(reportActions ?? {}); + } else { filteredReportActions = Object.entries(reportActions ?? {}) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); - } else { - filteredReportActions = Object.values(reportActions ?? {}); } const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9efb24c93b88..b1130af5d2ff 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -675,7 +675,6 @@ function ReportActionItem(props) { > {(hovered) => ( diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a181240e298f..62503f1a0cf9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -95,14 +95,14 @@ let listIDCount = Math.round(Math.random() * 100); * * @param {string} linkedID - ID of the linked message used for initial focus. * @param {array} messageArray - Array of messages. - * @param {function} fetchFn - Function to fetch more messages. + * @param {function} fetchNewerActon - Function to fetch more messages. * @param {string} route - Current route, used to reset states on route change. * @param {boolean} isLoading - Loading state indicator. * @param {object} reportScrollManager - Manages scrolling functionality. * @returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, reportScrollManager) => { +const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading, reportScrollManager) => { // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned const [edgeID, setEdgeID] = useState(''); const isCuttingForFirstRender = useRef(true); @@ -145,13 +145,14 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, report }, [linkedID, messageArray, index, isLoading, edgeID]); const hasMoreCashed = cattedArray.length < messageArray.length; + const newestReportAction = lodashGet(cattedArray, '[0]'); const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray if (!hasMoreCashed) { isCuttingForFirstRender.current = false; - fetchFn(); + fetchNewerActon(newestReportAction); } if (isCuttingForFirstRender.current) { // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. @@ -161,7 +162,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, report } setEdgeID(firstReportActionID); }, - [fetchFn, hasMoreCashed, reportScrollManager], + [fetchNewerActon, hasMoreCashed, reportScrollManager, newestReportAction], ); return { @@ -198,17 +199,15 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const throttledLoadNewerChats = useCallback( - () => { + const fetchNewerAction = useCallback( + (newestReportAction) => { if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { return; } - // eslint-disable-next-line no-use-before-define Report.getNewerActions(reportID, newestReportAction.reportActionID); }, - // eslint-disable-next-line no-use-before-define - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID], ); const { @@ -216,12 +215,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); + } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); - const isWeReachedTheOldestAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; + const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; /** * @returns {Boolean} @@ -328,12 +327,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } // Don't load more chats if we're already at the beginning of the chat history - if (!oldestReportAction || isWeReachedTheOldestAction) { + if (!oldestReportAction || hasCreatedAction) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, isWeReachedTheOldestAction, reportID]); + }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, hasCreatedAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const handleLoadNewerChats = useCallback( @@ -517,7 +516,7 @@ export default compose( withLocalize, withNetwork(), withOnyx({ - sesion: { + session: { key: ONYXKEYS.SESSION, }, }), From f18d81ab8a147d1830c8a1065e47dced8c2fcdda Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 21:47:13 +0100 Subject: [PATCH 064/124] add tests --- ...-native-web+0.19.9+004+fixLastSpacer.patch | 2 +- tests/unit/ReportActionsUtilsTest.js | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch index f5441d087277..08b5637a50c8 100644 --- a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch +++ b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index 7f6c880..b05da08 100644 +index faeb323..68d740a 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js @@ -78,14 +78,6 @@ function scrollEventThrottleOrDefault(scrollEventThrottle) { diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index 107941e32006..3a439f953579 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -256,6 +256,149 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); }); + describe('getContinuousReportActionChain', () => { + it('given an input ID of 1, ..., 7 it will return the report actions with id 1 - 7', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = [ + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 3); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = [ + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 14, ..., 17 it will return the report actions with id 14 - 17', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = [ + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 16); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 8 or 13 which are not exist in Onyx it will return an empty array', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = []; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + }); describe('getLastVisibleAction', () => { it('should return the last visible action for a report', () => { From 44bba8518bcf065348f3e6dd6402edf74cc0d04b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 22:29:15 +0100 Subject: [PATCH 065/124] add prop types --- src/pages/home/ReportScreen.js | 20 +++++++++----------- src/pages/home/report/ReportActionsList.js | 5 +++-- tests/unit/ReportActionsUtilsTest.js | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 90bb3d80df74..ebe8dd309392 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -19,7 +19,6 @@ import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportI import withViewportOffsetTop from '@components/withViewportOffsetTop'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -65,6 +64,9 @@ const propTypes = { /** The report currently being looked at */ report: reportPropTypes, + /** Array of all report actions for this report */ + allReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + /** The report metadata loading states */ reportMetadata: reportMetadataPropTypes, @@ -162,7 +164,6 @@ function ReportScreen({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const {isOffline} = useNetwork(); const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); @@ -174,16 +175,13 @@ function ReportScreen({ const [isPrepareLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { - if (!!allReportActions && allReportActions.length === 0) { + if (_.isEmpty(allReportActions)) { return []; } - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, false); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); - // eslint-disable-next-line rulesdir/prefer-underscore-method - const reportActionsWithoutDeleted = currentRangeOfReportActions.filter((item) => ReportActionsUtils.shouldReportActionBeVisible(item, item.reportActionID)); - return reportActionsWithoutDeleted; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportActionIDFromRoute, allReportActions, isOffline]); + return _.filter(currentRangeOfReportActions, (reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); + }, [reportActionIDFromRoute, allReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); @@ -265,7 +263,7 @@ function ReportScreen({ return reportIDFromRoute !== '' && report.reportID && !isTransitioning; }, [report, reportIDFromRoute]); - const isShowReportActionList = useMemo( + const shouldShowReportActionList = useMemo( () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), [isReportReadyForDisplay, reportActions, isLoading, reportMetadata.isLoadingInitialReportActions], ); @@ -551,7 +549,7 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isShowReportActionList && ( + {shouldShowReportActionList && ( { {reportActionID: 12, previousReportActionID: 11}, ]; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 10); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); From 9a54d64c96fb5c0549d7528d9b53d32e6426425b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 11 Jan 2024 18:13:31 -0500 Subject: [PATCH 066/124] MVCPFlatList fixes --- src/components/FlatList/MVCPFlatList.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 44cb50b98e11..5131f1cc2c49 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -67,12 +67,13 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont } const scrollOffset = getScrollOffset(); + lastScrollOffsetRef.current = scrollOffset; const contentViewLength = contentView.childNodes.length; for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) { const subview = contentView.childNodes[i]; const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; - if (subviewOffset > scrollOffset || i === contentViewLength - 1) { + if (subviewOffset > scrollOffset) { prevFirstVisibleOffsetRef.current = subviewOffset; firstVisibleViewRef.current = subview; break; @@ -125,6 +126,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont } adjustForMaintainVisibleContentPosition(); + prepareForMaintainVisibleContentPosition(); }); }); mutationObserver.observe(contentView, { @@ -134,7 +136,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont }); mutationObserverRef.current = mutationObserver; - }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); + }, [adjustForMaintainVisibleContentPosition, prepareForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); React.useEffect(() => { requestAnimationFrame(() => { @@ -168,13 +170,11 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont const onScrollInternal = React.useCallback( (ev) => { - lastScrollOffsetRef.current = getScrollOffset(); - prepareForMaintainVisibleContentPosition(); onScroll?.(ev); }, - [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll], + [prepareForMaintainVisibleContentPosition, onScroll], ); return ( From a4d9a2e005ebce5882e86f1c02bb4c37a193b044 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 11 Jan 2024 23:37:45 -0500 Subject: [PATCH 067/124] More fixes --- src/components/FlatList/MVCPFlatList.js | 8 +++----- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 2 -- src/pages/home/report/ReportActionsView.js | 5 +---- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 5131f1cc2c49..b738dedd91af 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -138,11 +138,9 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont mutationObserverRef.current = mutationObserver; }, [adjustForMaintainVisibleContentPosition, prepareForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); - React.useEffect(() => { - requestAnimationFrame(() => { - prepareForMaintainVisibleContentPosition(); - setupMutationObserver(); - }); + React.useLayoutEffect(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); const setMergedRef = useMergeRefs(scrollRef, forwardedRef); diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index fe7b9bba463e..48401d68c50c 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -4,7 +4,6 @@ import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; const WINDOW_SIZE = 21; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( @@ -15,7 +14,6 @@ function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 62503f1a0cf9..05d156347ba0 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -155,14 +155,11 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading fetchNewerActon(newestReportAction); } if (isCuttingForFirstRender.current) { - // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. - // We manually trigger a scroll to a slight offset to ensure the expected scroll behavior. - reportScrollManager.scrollToOffsetWithoutAnimation(1); isCuttingForFirstRender.current = false; } setEdgeID(firstReportActionID); }, - [fetchNewerActon, hasMoreCashed, reportScrollManager, newestReportAction], + [fetchNewerActon, hasMoreCashed, newestReportAction], ); return { From 1f10abb207219d3d8def8acb4a97a3e66b10db02 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 12 Jan 2024 00:04:16 -0500 Subject: [PATCH 068/124] Use minIndexForVisible 1 to dodge loading views --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 48401d68c50c..e686b7441fb5 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -13,7 +13,7 @@ function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef From a07c9a92c3c3f4b44cab87b1482c555b6e7e8f3f Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 12 Jan 2024 00:06:07 -0500 Subject: [PATCH 069/124] Add comment --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e686b7441fb5..8c087a46fe3a 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -13,6 +13,7 @@ function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef Date: Fri, 12 Jan 2024 00:06:36 -0500 Subject: [PATCH 070/124] Remove windowSize since 21 is the default --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 8c087a46fe3a..b3e996cc4e85 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -3,15 +3,12 @@ import React, {forwardRef} from 'react'; import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; -const WINDOW_SIZE = 21; - function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( Date: Fri, 12 Jan 2024 00:09:28 -0500 Subject: [PATCH 071/124] Fix lint --- src/pages/home/report/ReportActionsView.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 05d156347ba0..f577d9d3b6fa 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -98,11 +98,10 @@ let listIDCount = Math.round(Math.random() * 100); * @param {function} fetchNewerActon - Function to fetch more messages. * @param {string} route - Current route, used to reset states on route change. * @param {boolean} isLoading - Loading state indicator. - * @param {object} reportScrollManager - Manages scrolling functionality. * @returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading, reportScrollManager) => { +const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading) => { // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned const [edgeID, setEdgeID] = useState(''); const isCuttingForFirstRender = useRef(true); @@ -123,7 +122,7 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading return -1; } - return messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); + return _.findIndex(messageArray, (obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); }, [messageArray, edgeID, linkedID]); const cattedArray = useMemo(() => { @@ -136,10 +135,9 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); - } else { - const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; - return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } + const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; + return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; // edgeID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); @@ -212,7 +210,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); + } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); From d08dea532480085710e2acc190401eca7af9ff1c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 15:47:43 +0100 Subject: [PATCH 072/124] fix issue with navigating between different reports when actions are cached --- src/pages/home/ReportScreen.js | 17 ++++++++--------- src/pages/home/report/ReportActionsView.js | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index ebe8dd309392..527092990583 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -173,7 +173,7 @@ function ReportScreen({ const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isPrepareLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); - + const isFocused = useIsFocused(); const reportActions = useMemo(() => { if (_.isEmpty(allReportActions)) { return []; @@ -183,6 +183,7 @@ function ReportScreen({ return _.filter(currentRangeOfReportActions, (reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); }, [reportActionIDFromRoute, allReportActions]); + const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -263,15 +264,17 @@ function ReportScreen({ return reportIDFromRoute !== '' && report.reportID && !isTransitioning; }, [report, reportIDFromRoute]); + const shouldShowSkeleton = + isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); + const shouldShowReportActionList = useMemo( - () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), - [isReportReadyForDisplay, reportActions, isLoading, reportMetadata.isLoadingInitialReportActions], + () => isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading, + [isReportReadyForDisplay, isLoading, isLoadingInitialReportActions], ); const fetchReport = useCallback(() => { Report.openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); - const isFocused = useIsFocused(); useEffect(() => { if (!report.reportID || !isFocused) { @@ -484,11 +487,6 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); - const shouldShowSkeleton = useMemo( - () => isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoading || reportMetadata.isLoadingInitialReportActions, - [isPrepareLinkingToMessage, isReportReadyForDisplay, isLoading, reportMetadata.isLoadingInitialReportActions], - ); - // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. useEffect(() => { if (reportMetadata.isLoadingInitialReportActions && shouldTriggerLoadingRef.current) { @@ -560,6 +558,7 @@ function ReportScreen({ isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} isComposerFullSize={isComposerFullSize} policy={policy} + isContentReady={!shouldShowSkeleton} /> )} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f577d9d3b6fa..946006b46030 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -108,7 +108,7 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading useLayoutEffect(() => { setEdgeID(''); - }, [route, linkedID]); + }, [route]); const listID = useMemo(() => { isCuttingForFirstRender.current = true; @@ -118,12 +118,12 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading }, [route]); const index = useMemo(() => { - if (!linkedID) { + if (!linkedID || isLoading) { return -1; } return _.findIndex(messageArray, (obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); - }, [messageArray, edgeID, linkedID]); + }, [messageArray, edgeID, linkedID, isLoading]); const cattedArray = useMemo(() => { if (!linkedID) { @@ -142,13 +142,13 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); - const hasMoreCashed = cattedArray.length < messageArray.length; + const hasMoreCached = cattedArray.length < messageArray.length; const newestReportAction = lodashGet(cattedArray, '[0]'); const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray - if (!hasMoreCashed) { + if (!hasMoreCached) { isCuttingForFirstRender.current = false; fetchNewerActon(newestReportAction); } @@ -157,7 +157,7 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading } setEdgeID(firstReportActionID); }, - [fetchNewerActon, hasMoreCashed, newestReportAction], + [fetchNewerActon, hasMoreCached, newestReportAction], ); return { @@ -181,14 +181,14 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); const {windowHeight} = useWindowDimensions(); + const isFocused = useIsFocused(); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); - - const isFocused = useIsFocused(); const reportID = props.report.reportID; + const isLoading = (!!reportActionID && props.isLoadingInitialReportActions)|| !props.isContentReady; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -210,7 +210,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions); + } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); @@ -412,6 +412,9 @@ ReportActionsView.defaultProps = defaultProps; ReportActionsView.displayName = 'ReportActionsView'; function arePropsEqual(oldProps, newProps) { + if (!_.isEqual(oldProps.isContentReady, newProps.isContentReady)) { + return false; + } if (!_.isEqual(oldProps.reportActions, newProps.reportActions)) { return false; } From 7533b5118d99f5b42b0ffbb857e43484f1da44a9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 16:39:46 +0100 Subject: [PATCH 073/124] delete scrollToOffsetWithoutAnimation --- src/hooks/useReportScrollManager/index.native.ts | 16 +--------------- src/hooks/useReportScrollManager/index.ts | 16 +--------------- src/hooks/useReportScrollManager/types.ts | 1 - 3 files changed, 2 insertions(+), 31 deletions(-) diff --git a/src/hooks/useReportScrollManager/index.native.ts b/src/hooks/useReportScrollManager/index.native.ts index 0af995ddc1f0..6666a4ebd0f2 100644 --- a/src/hooks/useReportScrollManager/index.native.ts +++ b/src/hooks/useReportScrollManager/index.native.ts @@ -29,21 +29,7 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current?.scrollToOffset({animated: false, offset: 0}); }, [flatListRef, setScrollPosition]); - /** - * Scroll to the offset of the flatlist. - */ - const scrollToOffsetWithoutAnimation = useCallback( - (offset: number) => { - if (!flatListRef?.current) { - return; - } - - flatListRef.current.scrollToOffset({animated: false, offset}); - }, - [flatListRef], - ); - - return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; + return {ref: flatListRef, scrollToIndex, scrollToBottom}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts index d9b3605b9006..8b56cd639d08 100644 --- a/src/hooks/useReportScrollManager/index.ts +++ b/src/hooks/useReportScrollManager/index.ts @@ -28,21 +28,7 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, [flatListRef]); - /** - * Scroll to the bottom of the flatlist. - */ - const scrollToOffsetWithoutAnimation = useCallback( - (offset: number) => { - if (!flatListRef?.current) { - return; - } - - flatListRef.current.scrollToOffset({animated: false, offset}); - }, - [flatListRef], - ); - - return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; + return {ref: flatListRef, scrollToIndex, scrollToBottom}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/types.ts b/src/hooks/useReportScrollManager/types.ts index f29b5dfd44a2..5182f7269a9c 100644 --- a/src/hooks/useReportScrollManager/types.ts +++ b/src/hooks/useReportScrollManager/types.ts @@ -4,7 +4,6 @@ type ReportScrollManagerData = { ref: FlatListRefType; scrollToIndex: (index: number, isEditing?: boolean) => void; scrollToBottom: () => void; - scrollToOffsetWithoutAnimation: (offset: number) => void; }; export default ReportScrollManagerData; From 9478b8039e3737beaa9d72824f0cd035614bfebe Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 16:40:20 +0100 Subject: [PATCH 074/124] delete getReportActionsWithoutRemoved --- src/libs/ReportActionsUtils.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 838c6e4f646f..1c634584178a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -216,9 +216,12 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } -// Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. -// See unit tests for example of inputs and expected outputs. -// Note: sortedReportActions sorted in descending order + +/** + * Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. + * See unit tests for example of inputs and expected outputs. + * Note: sortedReportActions sorted in descending order + */ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; @@ -541,13 +544,6 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null, s return getSortedReportActions(baseURLAdjustedReportActions, true); } -function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { - if (!reportActions) { - return []; - } - return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); -} - /** * In some cases, there can be multiple closed report actions in a chat report. * This method returns the last closed report action so we can always show the correct archived report reason. @@ -873,7 +869,6 @@ export { getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, - getReportActionsWithoutRemoved, isConsecutiveActionMadeByPreviousActor, isCreatedAction, isCreatedTaskReportAction, From 67d0dc822bdeed78ecf6b3c028ddb06f9ee86500 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 17:41:21 +0100 Subject: [PATCH 075/124] renaming and adding comments to pagination handler --- src/libs/actions/Report.ts | 2 +- src/pages/home/ReportScreen.js | 2 +- src/pages/home/report/ReportActionsView.js | 85 +++++++++++----------- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b53684c0b6e7..b9365c0c900a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -465,7 +465,7 @@ function reportActionsExist(reportID: string): boolean { * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * * @param reportID The ID of the report to open - * @param reportActionID The ID of the report action to navigate to + * @param reportActionID The ID used to fetch a specific range of report actions related to the current reportActionID when opening a chat. * @param participantLoginList The list of users that are included in a new chat, not including the user creating it * @param newReportObject The optimistic report object created when making a new chat, saved as optimistic data * @param parentReportActionID The parent report action that a thread was created from (only passed for new threads) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 527092990583..eb97af5a6090 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -109,6 +109,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, parentReportAction: {}, + allReportActions: {}, report: {}, reportMetadata: { isLoadingInitialReportActions: true, @@ -551,7 +552,6 @@ function ReportScreen({ { - // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned - const [edgeID, setEdgeID] = useState(''); - const isCuttingForFirstRender = useRef(true); +const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerReportActions, route, isLoading) => { + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); useLayoutEffect(() => { - setEdgeID(''); + setCurrentReportActionID(''); }, [route]); const listID = useMemo(() => { - isCuttingForFirstRender.current = true; + isFirstLinkedActionRender.current = true; listIDCount += 1; return listIDCount; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -122,53 +122,53 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading return -1; } - return _.findIndex(messageArray, (obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); - }, [messageArray, edgeID, linkedID, isLoading]); + return _.findIndex(allReportActions, (obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); + }, [allReportActions, currentReportActionID, linkedID, isLoading]); - const cattedArray = useMemo(() => { + const visibleReportActions = useMemo(() => { if (!linkedID) { - return messageArray; + return allReportActions; } if (isLoading || index === -1) { return []; } - if (isCuttingForFirstRender.current) { - return messageArray.slice(index, messageArray.length); + if (isFirstLinkedActionRender.current) { + return allReportActions.slice(index, allReportActions.length); } const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; - return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; - // edgeID is needed to trigger batching once the report action has been positioned + return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; + // currentReportActionID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, messageArray, index, isLoading, edgeID]); + }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); - const hasMoreCached = cattedArray.length < messageArray.length; - const newestReportAction = lodashGet(cattedArray, '[0]'); + const hasMoreCached = visibleReportActions.length < allReportActions.length; + const newestReportAction = lodashGet(visibleReportActions, '[0]'); - const paginate = useCallback( + const handleReportActionPagination = useCallback( ({firstReportActionID}) => { - // This function is a placeholder as the actual pagination is handled by cattedArray + // This function is a placeholder as the actual pagination is handled by visibleReportActions if (!hasMoreCached) { - isCuttingForFirstRender.current = false; - fetchNewerActon(newestReportAction); + isFirstLinkedActionRender.current = false; + fetchNewerReportActions(newestReportAction); } - if (isCuttingForFirstRender.current) { - isCuttingForFirstRender.current = false; + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; } - setEdgeID(firstReportActionID); + setCurrentReportActionID(firstReportActionID); }, - [fetchNewerActon, hasMoreCached, newestReportAction], + [fetchNewerReportActions, hasMoreCached, newestReportAction], ); return { - cattedArray, - fetchFunc: paginate, + visibleReportActions, + loadMoreReportActionsHandler: handleReportActionPagination, linkedIdIndex: index, listID, }; }; -function ReportActionsView({reportActions: allReportActions, fetchReport, ...props}) { +function ReportActionsView({reportActions: allReportActions, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); @@ -188,7 +188,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; - const isLoading = (!!reportActionID && props.isLoadingInitialReportActions)|| !props.isContentReady; + const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isContentReady; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -206,11 +206,11 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro ); const { - cattedArray: reportActions, - fetchFunc, + visibleReportActions: reportActions, + loadMoreReportActionsHandler, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); + } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); @@ -245,6 +245,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } + // This function is triggered when a user clicks on a link to navigate to a report. + // For each link click, we retrieve the report data again, even though it may already be cached. + // There should be only one openReport execution per page start or navigating Report.openReport(reportID, reportActionID); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); @@ -330,7 +333,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, hasCreatedAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); - const handleLoadNewerChats = useCallback( + const loadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { @@ -338,7 +341,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } const isContentSmallerThanList = checkIfContentSmallerThanList(); if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { - fetchFunc({firstReportActionID}); + loadMoreReportActionsHandler({firstReportActionID}); } }, [ @@ -348,7 +351,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro reportActionID, linkedIdIndex, hasNewestReportAction, - fetchFunc, + loadMoreReportActionsHandler, firstReportActionID, props.network.isOffline, ], @@ -392,7 +395,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro sortedReportActions={reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} - loadNewerChats={handleLoadNewerChats} + loadNewerChats={loadNewerChats} isLinkingLoader={!!reportActionID && props.isLoadingInitialReportActions} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} From 9c743dcd0b75593e975e23349c6e426eb0c73b30 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 17:45:56 +0100 Subject: [PATCH 076/124] cleanup --- .../getInitialNumToRender/index.native.ts | 0 src/libs/getInitialNumToRender/index.ts | 5 +++++ src/pages/home/report/ReportActionsList.js | 9 ++------- src/pages/home/report/getInitialNumToRender/index.ts | 4 ---- 4 files changed, 7 insertions(+), 11 deletions(-) rename src/{pages/home/report => libs}/getInitialNumToRender/index.native.ts (100%) create mode 100644 src/libs/getInitialNumToRender/index.ts delete mode 100644 src/pages/home/report/getInitialNumToRender/index.ts diff --git a/src/pages/home/report/getInitialNumToRender/index.native.ts b/src/libs/getInitialNumToRender/index.native.ts similarity index 100% rename from src/pages/home/report/getInitialNumToRender/index.native.ts rename to src/libs/getInitialNumToRender/index.native.ts diff --git a/src/libs/getInitialNumToRender/index.ts b/src/libs/getInitialNumToRender/index.ts new file mode 100644 index 000000000000..62b6d6dee275 --- /dev/null +++ b/src/libs/getInitialNumToRender/index.ts @@ -0,0 +1,5 @@ +function getInitialNumToRender(numToRender: number): number { + // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. + return Math.max(numToRender, 50); +} +export default getInitialNumToRender; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index cdeaebf5f2bc..35bb851deca3 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -14,7 +14,7 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; -import getPlatform from '@libs/getPlatform'; +import getInitialNumToRender from '@libs/getInitialNumToRender'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -25,7 +25,6 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import FloatingMessageCounter from './FloatingMessageCounter'; -import getInitialNumToRender from './getInitialNumToRender/index'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; @@ -157,8 +156,6 @@ function ReportActionsList({ } return cacheUnreadMarkers.get(report.reportID); }; - const platform = getPlatform(); - const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID; const [currentUnreadMarker, setCurrentUnreadMarker] = useState(markerInit); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); @@ -362,12 +359,10 @@ function ReportActionsList({ const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); const numToRender = Math.ceil(availableHeight / minimumReportActionHeight); if (linkedReportActionID) { - // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. - return getInitialNumToRender(numToRender); } return numToRender; - }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight, linkedReportActionID, isNative]); + }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight, linkedReportActionID]); /** * Thread's divider line should hide when the first chat in the thread is marked as unread. diff --git a/src/pages/home/report/getInitialNumToRender/index.ts b/src/pages/home/report/getInitialNumToRender/index.ts deleted file mode 100644 index eb94492d6ad4..000000000000 --- a/src/pages/home/report/getInitialNumToRender/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -function getInitialNumToRender(numToRender: number): number { - return Math.max(numToRender, 50); -} -export default getInitialNumToRender; From 8887f7c0c3c6079c40ab3e2e5fc5ca858f6b732e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 18:14:12 +0100 Subject: [PATCH 077/124] clean artifacts from patch --- patches/react-native-web+0.19.9+004+fixLastSpacer.patch | 2 +- src/pages/home/ReportScreen.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch index 08b5637a50c8..fc48c00094dc 100644 --- a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch +++ b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch @@ -17,7 +17,7 @@ index faeb323..68d740a 100644 /** * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) -@@ -1107,7 +1099,8 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1119,7 +1111,8 @@ class VirtualizedList extends StateSafePureComponent { _keylessItemComponentName = ''; var spacerKey = this._getSpacerKey(!horizontal); var renderRegions = this.state.renderMask.enumerateRegions(); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index eb97af5a6090..d244cab25c8c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -64,8 +64,8 @@ const propTypes = { /** The report currently being looked at */ report: reportPropTypes, - /** Array of all report actions for this report */ - allReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + /** All the report actions for this report */ + allReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), /** The report metadata loading states */ reportMetadata: reportMetadataPropTypes, From c2fc7730f6ee12622f43e16a1038f1ba197428d6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 14 Jan 2024 14:37:01 +0100 Subject: [PATCH 078/124] add optional autoscrollToTopThreshold --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index b3e996cc4e85..e2d52b9b16d1 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -3,15 +3,23 @@ import React, {forwardRef} from 'react'; import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; -function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { +type BaseInvertedFlatListProps = FlatListProps & { + enableAutoscrollToTopThreshold?: boolean; +}; + +const AUTOSCROLL_TO_TOP_THRESHOLD = 128; + +function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { + const {enableAutoscrollToTopThreshold, ...rest} = props; return ( From dbcc38b0b6dfb5c794a2e5fa4af74a9147f4da7e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 14 Jan 2024 15:18:00 +0100 Subject: [PATCH 079/124] avoid scrollToBottom while linking --- src/pages/home/ReportScreen.js | 5 +--- src/pages/home/report/ReportActionsList.js | 27 ++++++++++++++-------- src/pages/home/report/ReportActionsView.js | 1 + 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d244cab25c8c..a0d072b421ea 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -268,10 +268,7 @@ function ReportScreen({ const shouldShowSkeleton = isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); - const shouldShowReportActionList = useMemo( - () => isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading, - [isReportReadyForDisplay, isLoading, isLoadingInitialReportActions], - ); + const shouldShowReportActionList = isReportReadyForDisplay && !isLoading; const fetchReport = useCallback(() => { Report.openReport(reportIDFromRoute, reportActionIDFromRoute); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 35bb851deca3..9bdaa428cce3 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -142,6 +142,7 @@ function ReportActionsList({ listID, onContentSizeChange, reportScrollManager, + enableAutoscrollToTopThreshold, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -189,12 +190,12 @@ function ReportActionsList({ }, [opacity]); useEffect(() => { - if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length) { + if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) { reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager]); + }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager, hasNewestReportAction]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because @@ -277,6 +278,18 @@ function ReportActionsList({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const scrollToBottomForCurrentUserAction = useCallback( + (isFromCurrentUser) => { + // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where + // they are now in the list. + if (!isFromCurrentUser || !hasNewestReportAction) { + return; + } + InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); + }, + [hasNewestReportAction, reportScrollManager], + ); + useEffect(() => { // Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function? // Answer: On web, when navigating to another report screen, the previous report screen doesn't get unmounted, @@ -292,14 +305,7 @@ function ReportActionsList({ // This callback is triggered when a new action arrives via Pusher and the event is emitted from Report.js. This allows us to maintain // a single source of truth for the "new action" event instead of trying to derive that a new action has appeared from looking at props. - const unsubscribe = Report.subscribeToNewActionEvent(report.reportID, (isFromCurrentUser) => { - // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where - // they are now in the list. - if (!isFromCurrentUser) { - return; - } - InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); - }); + const unsubscribe = Report.subscribeToNewActionEvent(report.reportID, scrollToBottomForCurrentUserAction); const cleanup = () => { if (unsubscribe) { @@ -529,6 +535,7 @@ function ReportActionsList({ onScrollToIndexFailed={() => {}} extraData={extraData} key={listID} + enableAutoscrollToTopThreshold={enableAutoscrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index bc06b22b8354..417b35e42ec7 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -404,6 +404,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { listID={listID} onContentSizeChange={onContentSizeChange} reportScrollManager={reportScrollManager} + enableAutoscrollToTopThreshold={hasNewestReportAction} /> From fbd2c9dc192a5088671bfa832f651215fcf0ad06 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 10:52:09 +0100 Subject: [PATCH 080/124] ensure Automatic Scrolling Works with Comment Linking --- src/pages/home/report/ReportActionsList.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 9bdaa428cce3..38bf80a8059e 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -176,6 +176,10 @@ function ReportActionsList({ const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); + const isLastPendingActionIsAdd = lodashGet(sortedVisibleReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + + // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. + const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd && hasNewestReportAction; // 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 @@ -190,12 +194,15 @@ function ReportActionsList({ }, [opacity]); useEffect(() => { - if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) { + if ( + (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) || + isNewestActionAvailableAndPendingAdd + ) { reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager, hasNewestReportAction]); + }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, isLastPendingActionIsAdd, linkedReportActionID, isNewestActionAvailableAndPendingAdd]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because From f64e8c818a5222553fb51c334cb7947e267c524d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 10:53:25 +0100 Subject: [PATCH 081/124] correct linking Issue for the first message in chat --- src/pages/home/report/ReportActionsView.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 417b35e42ec7..a5f293ec340f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -339,8 +339,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { return; } - const isContentSmallerThanList = checkIfContentSmallerThanList(); - if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { + // Determines if loading older reports is necessary when the content is smaller than the list + // and there are fewer than 23 items, indicating we've reached the oldest message. + const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; + + if ( + (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || + (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) + ) { loadMoreReportActionsHandler({firstReportActionID}); } }, @@ -354,6 +360,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { loadMoreReportActionsHandler, firstReportActionID, props.network.isOffline, + reportActions.length, ], ); From 2d77e7d24b66efb9480093566e8443fb3a7724ce Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 12:09:12 +0100 Subject: [PATCH 082/124] fix test --- src/libs/ReportActionsUtils.ts | 2 +- src/pages/home/report/ReportActionsView.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1c634584178a..ae5b4e0b5500 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -257,7 +257,7 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? startIndex--; } - return sortedReportActions.slice(startIndex, endIndex + 1); + return sortedReportActions.slice(startIndex, id ? endIndex + 1 : sortedReportActions.length); } /** diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a5f293ec340f..d7448d5a3bcd 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -411,7 +411,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { listID={listID} onContentSizeChange={onContentSizeChange} reportScrollManager={reportScrollManager} - enableAutoscrollToTopThreshold={hasNewestReportAction} + enableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} /> From 052087626b84c00503e82ec64b19da12c6c207f8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 15:39:27 +0100 Subject: [PATCH 083/124] fix console warning --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e2d52b9b16d1..7405462c585e 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -1,26 +1,38 @@ import type {ForwardedRef} from 'react'; -import React, {forwardRef} from 'react'; +import React, {forwardRef, useMemo} from 'react'; import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; type BaseInvertedFlatListProps = FlatListProps & { enableAutoscrollToTopThreshold?: boolean; }; - +type VisibleContentPositionConfig = { + minIndexForVisible: number; + autoscrollToTopThreshold?: number; +}; const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { const {enableAutoscrollToTopThreshold, ...rest} = props; + + const maintainVisibleContentPosition = useMemo(() => { + const config: VisibleContentPositionConfig = { + // This needs to be 1 to avoid using loading views as anchors. + minIndexForVisible: 1, + }; + + if (enableAutoscrollToTopThreshold) { + config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; + } + + return config; + }, [enableAutoscrollToTopThreshold]); return ( ); From 008b7fcfd49afe23292c73073fea87b0b25c9399 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 19:07:10 +0100 Subject: [PATCH 084/124] correct scrolling issue during initial chat load --- src/pages/home/ReportScreen.js | 2 +- src/pages/home/report/ReportActionsView.js | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 4870d69fe39e..b12faf24dfc7 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -557,7 +557,7 @@ function ReportScreen({ isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} isComposerFullSize={isComposerFullSize} policy={policy} - isContentReady={!shouldShowSkeleton} + isReadyForCommentLinking={!shouldShowSkeleton} /> )} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index d7448d5a3bcd..0972145a5eaa 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -98,10 +98,12 @@ let listIDCount = Math.round(Math.random() * 100); * @param {function} fetchNewerReportActions - Function to fetch more messages. * @param {string} route - Current route, used to reset states on route change. * @param {boolean} isLoading - Loading state indicator. + * @param {boolean} triggerListID - Used to trigger a listID change. * @returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerReportActions, route, isLoading) => { +const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerReportActions, route, isLoading, triggerListID) => { + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned const [currentReportActionID, setCurrentReportActionID] = useState(''); const isFirstLinkedActionRender = useRef(true); @@ -115,7 +117,7 @@ const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerRepo listIDCount += 1; return listIDCount; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route]); + }, [route, triggerListID]); const index = useMemo(() => { if (!linkedID || isLoading) { @@ -178,7 +180,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const didSubscribeToReportTypingEvents = useRef(false); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); - const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); const {windowHeight} = useWindowDimensions(); const isFocused = useIsFocused(); @@ -188,7 +189,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; - const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isContentReady; + const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -210,8 +211,8 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { loadMoreReportActionsHandler, linkedIdIndex, listID, - } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); - + } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, props.isLoadingInitialReportActions); + const hasCachedActions = useInitialValue(() => _.size(reportActions) > 0); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); @@ -320,7 +321,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { */ const loadOlderChats = useCallback(() => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. - if (props.network.isOffline || props.isLoadingOlderReportActions) { + if (props.network.isOffline || props.isLoadingOlderReportActions || props.isLoadingInitialReportActions) { return; } @@ -330,7 +331,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, hasCreatedAction, reportID]); + }, [props.network.isOffline, props.isLoadingOlderReportActions, props.isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const loadNewerChats = useCallback( @@ -423,7 +424,7 @@ ReportActionsView.defaultProps = defaultProps; ReportActionsView.displayName = 'ReportActionsView'; function arePropsEqual(oldProps, newProps) { - if (!_.isEqual(oldProps.isContentReady, newProps.isContentReady)) { + if (!_.isEqual(oldProps.isReadyForCommentLinking, newProps.isReadyForCommentLinking)) { return false; } if (!_.isEqual(oldProps.reportActions, newProps.reportActions)) { From 29f7f23294035eba945ce4f9b608d4c6d3c30b81 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 19:46:48 +0100 Subject: [PATCH 085/124] bring back reportScrollManager to ReportActionList --- src/pages/home/report/ReportActionsList.js | 3 ++- src/pages/home/report/ReportActionsView.js | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 38bf80a8059e..d2d00440eb8b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -11,6 +11,7 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useReportScrollManager from '@hooks/useReportScrollManager'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; @@ -141,7 +142,6 @@ function ReportActionsList({ isComposerFullSize, listID, onContentSizeChange, - reportScrollManager, enableAutoscrollToTopThreshold, }) { const styles = useThemeStyles(); @@ -150,6 +150,7 @@ function ReportActionsList({ const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); + const reportScrollManager = useReportScrollManager(); const markerInit = () => { if (!cacheUnreadMarkers.has(report.reportID)) { diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 0972145a5eaa..c838411fa80f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -11,7 +11,6 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; -import useReportScrollManager from '@hooks/useReportScrollManager'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; @@ -174,7 +173,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); - const reportScrollManager = useReportScrollManager(); const reportActionID = lodashGet(route, 'params.reportActionID', null); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); @@ -411,7 +409,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} - reportScrollManager={reportScrollManager} enableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} /> From 59aaf17a94c733eb078d92d831767561dd3b0b1f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 16 Jan 2024 13:33:43 +0100 Subject: [PATCH 086/124] update naming conventions and typing --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 18 ++++++++---------- src/pages/home/ReportScreen.js | 4 ++-- src/pages/home/report/ReportActionsList.js | 6 +++--- src/pages/home/report/ReportActionsView.js | 2 +- .../index.native.ts | 0 .../index.ts | 0 6 files changed, 14 insertions(+), 16 deletions(-) rename src/{libs/getInitialNumToRender => pages/home/report/getInitialNumReportActionsToRender}/index.native.ts (100%) rename src/{libs/getInitialNumToRender => pages/home/report/getInitialNumReportActionsToRender}/index.ts (100%) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 7405462c585e..ebb4d01d1f23 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -1,32 +1,30 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useMemo} from 'react'; -import type {FlatListProps} from 'react-native'; +import type {FlatListProps, ScrollViewProps} from 'react-native'; import FlatList from '@components/FlatList'; type BaseInvertedFlatListProps = FlatListProps & { - enableAutoscrollToTopThreshold?: boolean; -}; -type VisibleContentPositionConfig = { - minIndexForVisible: number; - autoscrollToTopThreshold?: number; + shouldEnableAutoscrollToTopThreshold?: boolean; }; + const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {enableAutoscrollToTopThreshold, ...rest} = props; + const {shouldEnableAutoscrollToTopThreshold, ...rest} = props; const maintainVisibleContentPosition = useMemo(() => { - const config: VisibleContentPositionConfig = { + const config: ScrollViewProps['maintainVisibleContentPosition'] = { // This needs to be 1 to avoid using loading views as anchors. minIndexForVisible: 1, }; - if (enableAutoscrollToTopThreshold) { + if (shouldEnableAutoscrollToTopThreshold) { config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; } return config; - }, [enableAutoscrollToTopThreshold]); + }, [shouldEnableAutoscrollToTopThreshold]); + return ( { if (_.isEmpty(allReportActions)) { @@ -268,7 +268,7 @@ function ReportScreen({ }, [report, reportIDFromRoute]); const shouldShowSkeleton = - isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); + isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); const shouldShowReportActionList = isReportReadyForDisplay && !isLoading; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index d2d00440eb8b..6aa0daa139ba 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -15,7 +15,6 @@ import useReportScrollManager from '@hooks/useReportScrollManager'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; -import getInitialNumToRender from '@libs/getInitialNumToRender'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -26,6 +25,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import FloatingMessageCounter from './FloatingMessageCounter'; +import getInitialNumToRender from './getInitialNumReportActionsToRender'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; @@ -142,7 +142,7 @@ function ReportActionsList({ isComposerFullSize, listID, onContentSizeChange, - enableAutoscrollToTopThreshold, + shouldEnableAutoscrollToTopThreshold, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -543,7 +543,7 @@ function ReportActionsList({ onScrollToIndexFailed={() => {}} extraData={extraData} key={listID} - enableAutoscrollToTopThreshold={enableAutoscrollToTopThreshold} + shouldEnableAutoscrollToTopThreshold={shouldEnableAutoscrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index c838411fa80f..e61ebf0f11f4 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -409,7 +409,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} - enableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} + shouldEnableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} /> diff --git a/src/libs/getInitialNumToRender/index.native.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.native.ts similarity index 100% rename from src/libs/getInitialNumToRender/index.native.ts rename to src/pages/home/report/getInitialNumReportActionsToRender/index.native.ts diff --git a/src/libs/getInitialNumToRender/index.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts similarity index 100% rename from src/libs/getInitialNumToRender/index.ts rename to src/pages/home/report/getInitialNumReportActionsToRender/index.ts From 00b041ca4d94fd0ddeaf925a28e34983420cff18 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 17 Jan 2024 15:32:08 +0100 Subject: [PATCH 087/124] fix typo and add CheckForPreviousReportActionID migration --- src/components/FlatList/MVCPFlatList.js | 2 +- src/libs/migrateOnyx.js | 3 ++- src/pages/home/report/ReportActionsList.js | 7 +++++-- .../report/getInitialNumReportActionsToRender/index.ts | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index b738dedd91af..abc3be4e2052 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -43,7 +43,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont if (scrollRef.current == null) { return 0; } - return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; + return horizontal ? scrollRef.current?.getScrollableNode().scrollLeft : scrollRef.current?.getScrollableNode().scrollTop; }, [horizontal]); const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode()?.childNodes[0], []); diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index 9b8b4056e3e5..036750fa5d4f 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -5,6 +5,7 @@ import PersonalDetailsByAccountID from './migrations/PersonalDetailsByAccountID' import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; +import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; export default function () { const startTime = Date.now(); @@ -12,7 +13,7 @@ export default function () { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [CheckForPreviousReportActionID, PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 6aa0daa139ba..dc58f5a42cbe 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -180,7 +180,7 @@ function ReportActionsList({ const isLastPendingActionIsAdd = lodashGet(sortedVisibleReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. - const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd && hasNewestReportAction; + const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd; // 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 @@ -199,7 +199,10 @@ function ReportActionsList({ (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) || isNewestActionAvailableAndPendingAdd ) { - reportScrollManager.scrollToBottom(); + // runAfterInteractions is used for isNewestActionAvailableAndPendingAdd + InteractionManager.runAfterInteractions(() => { + reportScrollManager.scrollToBottom(); + }); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; diff --git a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts index 62b6d6dee275..68ff8c4cab3f 100644 --- a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts +++ b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts @@ -1,5 +1,5 @@ function getInitialNumToRender(numToRender: number): number { - // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. + // For web and desktop environments, it's crucial to set this value equal to or higher than the maxToRenderPerBatch setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. return Math.max(numToRender, 50); } export default getInitialNumToRender; From cc523b2401a2171418761795792bb3a56461811f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 17 Jan 2024 15:47:06 +0100 Subject: [PATCH 088/124] fix 'new message' appearing on initial loading --- src/pages/home/report/ReportActionsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dc58f5a42cbe..b5e36c292643 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -519,7 +519,7 @@ function ReportActionsList({ return ( <> From f3b7090d9210b6e502d103870f22a6f453ad42d7 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 17 Jan 2024 15:59:40 +0100 Subject: [PATCH 089/124] lint --- src/libs/migrateOnyx.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index 036750fa5d4f..a2a846bddf5f 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -1,11 +1,11 @@ import _ from 'underscore'; import Log from './Log'; +import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; import PersonalDetailsByAccountID from './migrations/PersonalDetailsByAccountID'; import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; -import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; export default function () { const startTime = Date.now(); @@ -13,7 +13,14 @@ export default function () { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [CheckForPreviousReportActionID, PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [ + CheckForPreviousReportActionID, + PersonalDetailsByAccountID, + RenameReceiptFilename, + KeyReportActionsDraftByReportActionID, + TransactionBackupsToCollection, + RemoveEmptyReportActionsDrafts, + ]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. From c77d7b1427fb52331ed03d351e8d3c26758afc62 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 16:36:57 +0100 Subject: [PATCH 090/124] adjust gap handling in response to REPORTPREVIEW movement --- src/libs/ReportActionsUtils.ts | 2 +- src/pages/home/report/ReportActionsView.js | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fcf897292bb4..f6d8c139b802 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -257,7 +257,7 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? startIndex--; } - return sortedReportActions.slice(startIndex, id ? endIndex + 1 : sortedReportActions.length); + return sortedReportActions.slice(startIndex, endIndex + 1); } /** diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e61ebf0f11f4..123875f1f28a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -388,6 +388,33 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { [hasCachedActions], ); + useEffect(() => { + // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP + // This code should be removed once REPORTPREVIEW is no longer repositioned. + // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. + + const shouldOpenReport = + !hasCreatedAction && + props.isReadyForCommentLinking && + reportActions.length < 24 && + !props.isLoadingInitialReportAction && + !props.isLoadingOlderReportActions && + !props.isLoadingNewerReportActions; + + if (shouldOpenReport) { + Report.openReport(reportID, reportActionID); + } + }, [ + hasCreatedAction, + reportID, + reportActions, + reportActionID, + props.isReadyForCommentLinking, + props.isLoadingOlderReportActions, + props.isLoadingNewerReportActions, + props.isLoadingInitialReportAction, + ]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; From 2b8a4d788526576a411ad7db036b04338ca1ebdc Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 16:45:30 +0100 Subject: [PATCH 091/124] undo adjust gap handling in response to REPORTPREVIEW movement --- src/pages/home/report/ReportActionsView.js | 27 ---------------------- 1 file changed, 27 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 123875f1f28a..e61ebf0f11f4 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -388,33 +388,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { [hasCachedActions], ); - useEffect(() => { - // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP - // This code should be removed once REPORTPREVIEW is no longer repositioned. - // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. - - const shouldOpenReport = - !hasCreatedAction && - props.isReadyForCommentLinking && - reportActions.length < 24 && - !props.isLoadingInitialReportAction && - !props.isLoadingOlderReportActions && - !props.isLoadingNewerReportActions; - - if (shouldOpenReport) { - Report.openReport(reportID, reportActionID); - } - }, [ - hasCreatedAction, - reportID, - reportActions, - reportActionID, - props.isReadyForCommentLinking, - props.isLoadingOlderReportActions, - props.isLoadingNewerReportActions, - props.isLoadingInitialReportAction, - ]); - // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; From 104884a5d839d37137d95074c1ad54dc17e28b7c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 18:17:30 +0100 Subject: [PATCH 092/124] implement a blocking view when the linked link does not belong to the current report --- src/components/FlatList/MVCPFlatList.js | 2 +- src/pages/home/ReportScreen.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index abc3be4e2052..1b6bca14ecf3 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -43,7 +43,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont if (scrollRef.current == null) { return 0; } - return horizontal ? scrollRef.current?.getScrollableNode().scrollLeft : scrollRef.current?.getScrollableNode().scrollTop; + return horizontal ? scrollRef.current?.getScrollableNode()?.scrollLeft : scrollRef.current?.getScrollableNode()?.scrollTop; }, [horizontal]); const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode()?.childNodes[0], []); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index b111273b2085..9ae6dca7519d 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -6,8 +6,10 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; +import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; +import * as Illustrations from '@components/Icon/Illustrations'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -33,6 +35,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportMetadataPropTypes from '@pages/reportMetadataPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; +import variables from '@styles/variables'; import * as ComposerActions from '@userActions/Composer'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; @@ -503,6 +506,25 @@ function ReportScreen({ } }, [reportMetadata.isLoadingInitialReportActions]); + const onLinkPress = () => { + Navigation.setParams({reportActionID: ''}); + fetchReport(); + }; + + if (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage) { + return ( + + ); + } + return ( From 3ce108095265034916b8885a2af4351a1454a8fa Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 18:28:35 +0100 Subject: [PATCH 093/124] bring back 'adjust gap handling in response to REPORTPREVIEW movement' --- src/pages/home/report/ReportActionsView.js | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e61ebf0f11f4..3fd1fb44be20 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -388,6 +388,34 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { [hasCachedActions], ); + useEffect(() => { + // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP + // This code should be removed once REPORTPREVIEW is no longer repositioned. + // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. + + const shouldOpenReport = + !hasCreatedAction && + props.isReadyForCommentLinking && + reportActions.length < 24 && + reportActions.length > 1 && + !props.isLoadingInitialReportAction && + !props.isLoadingOlderReportActions && + !props.isLoadingNewerReportActions; + + if (shouldOpenReport) { + Report.openReport(reportID, reportActionID); + } + }, [ + hasCreatedAction, + reportID, + reportActions, + reportActionID, + props.isReadyForCommentLinking, + props.isLoadingOlderReportActions, + props.isLoadingNewerReportActions, + props.isLoadingInitialReportAction, + ]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; From 825a5439fad4d931ac32fb9d9a4b4c4331e3528b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 16:54:20 +0100 Subject: [PATCH 094/124] lint --- src/pages/home/report/ReportActionsView.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3fd1fb44be20..8b95131dea42 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -188,6 +188,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; + const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -211,8 +212,8 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { listID, } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, props.isLoadingInitialReportActions); const hasCachedActions = useInitialValue(() => _.size(reportActions) > 0); - const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; - const newestReportAction = lodashGet(reportActions, '[0]'); + const hasNewestReportAction = lodashGet(reportActions, ['0', 'created']) === props.report.lastVisibleActionCreated; + const newestReportAction = lodashGet(reportActions, ['0']); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; @@ -394,6 +395,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. const shouldOpenReport = + firstReportActionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && !hasCreatedAction && props.isReadyForCommentLinking && reportActions.length < 24 && @@ -410,6 +412,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { reportID, reportActions, reportActionID, + firstReportActionName, props.isReadyForCommentLinking, props.isLoadingOlderReportActions, props.isLoadingNewerReportActions, From e7ee260d308d5827a1dcf8aee8f972a076e2fb06 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 17:06:31 +0100 Subject: [PATCH 095/124] lint after merge --- src/pages/home/report/ReportActionsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8b95131dea42..a08a202f845a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -188,7 +188,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; - const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -216,6 +215,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const newestReportAction = lodashGet(reportActions, ['0']); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; + const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); /** * @returns {Boolean} From 07929ee2966356371516c2a747a787c251cf35f5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 18:06:03 +0100 Subject: [PATCH 096/124] fix test --- tests/ui/UnreadIndicatorsTest.js | 18 +++++++++--------- tests/utils/TestHelper.js | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index e4d4d877f66b..97d50fb392f3 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -249,15 +249,15 @@ function signInAndGetAppWithUnreadChat() { }, ], }, - 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1'), - 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2'), - 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3'), - 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4'), - 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5'), - 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6'), - 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7'), - 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8'), - 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9'), + 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), + 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), + 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), + 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), + 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), + 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), + 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), + 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), + 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), }); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index dd95ab4efb67..4a331496541a 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -198,9 +198,10 @@ function setPersonalDetails(login, accountID) { * @param {String} created * @param {Number} actorAccountID * @param {String} actionID + * @param {String} previousReportActionID * @returns {Object} */ -function buildTestReportComment(created, actorAccountID, actionID = null) { +function buildTestReportComment(created, actorAccountID, actionID = null, previousReportActionID = null) { const reportActionID = actionID || NumberUtils.rand64(); return { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, @@ -209,6 +210,7 @@ function buildTestReportComment(created, actorAccountID, actionID = null) { message: [{type: 'COMMENT', html: `Comment ${actionID}`, text: `Comment ${actionID}`}], reportActionID, actorAccountID, + previousReportActionID, }; } From c1c260fb20dad42c5ee43b84a80cdcc1686dd5df Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 20 Jan 2024 18:15:31 +0100 Subject: [PATCH 097/124] refactor autosScrollToTopThreshold --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 8 +++---- src/pages/home/report/ReportActionsList.js | 4 ++-- src/pages/home/report/ReportActionsView.js | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index ebb4d01d1f23..e007f63c8e97 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -4,13 +4,13 @@ import type {FlatListProps, ScrollViewProps} from 'react-native'; import FlatList from '@components/FlatList'; type BaseInvertedFlatListProps = FlatListProps & { - shouldEnableAutoscrollToTopThreshold?: boolean; + shouldEnableAutoScrollToTopThreshold?: boolean; }; const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {shouldEnableAutoscrollToTopThreshold, ...rest} = props; + const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; const maintainVisibleContentPosition = useMemo(() => { const config: ScrollViewProps['maintainVisibleContentPosition'] = { @@ -18,12 +18,12 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa minIndexForVisible: 1, }; - if (shouldEnableAutoscrollToTopThreshold) { + if (shouldEnableAutoScrollToTopThreshold) { config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; } return config; - }, [shouldEnableAutoscrollToTopThreshold]); + }, [shouldEnableAutoScrollToTopThreshold]); return ( {}} extraData={extraData} key={listID} - shouldEnableAutoscrollToTopThreshold={shouldEnableAutoscrollToTopThreshold} + shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a08a202f845a..00584859c4f9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -2,6 +2,7 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -184,7 +185,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); - + const [isInitialLinkedView, setIsInitialLinkedView] = useState(false); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; @@ -393,13 +394,12 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP // This code should be removed once REPORTPREVIEW is no longer repositioned. // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. - const shouldOpenReport = firstReportActionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && !hasCreatedAction && props.isReadyForCommentLinking && reportActions.length < 24 && - reportActions.length > 1 && + reportActions.length >= 1 && !props.isLoadingInitialReportAction && !props.isLoadingOlderReportActions && !props.isLoadingNewerReportActions; @@ -419,10 +419,26 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { props.isLoadingInitialReportAction, ]); + // Check if the first report action in the list is the one we're currently linked to + const isTheFirstReportActionIsLinked = firstReportActionID !== reportActionID; + + useEffect(() => { + if (isTheFirstReportActionIsLinked) { + // this should be applied after we navigated to linked reportAction + InteractionManager.runAfterInteractions(() => { + setIsInitialLinkedView(true); + }); + } else { + setIsInitialLinkedView(false); + } + }, [isTheFirstReportActionIsLinked]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; } + // AutoScroll is disabled when we do linking to a specific reportAction + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || isInitialLinkedView); return ( <> @@ -440,7 +456,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} - shouldEnableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} + shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll} /> From 14ab0d60bebe946ac1a14de387fdefbf4ddd7b3c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 23 Jan 2024 14:41:20 +0100 Subject: [PATCH 098/124] determine if a linked report action is deleted --- src/pages/home/ReportScreen.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 9ae6dca7519d..2a62cdf43466 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -511,7 +511,14 @@ function ReportScreen({ fetchReport(); }; - if (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage) { + const isLinkedReportActionDeleted = useMemo(() => { + if (!reportActionIDFromRoute) { + return false; + } + return ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); + }, [reportActionIDFromRoute, allReportActions]); + + if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage)) { return ( Date: Wed, 24 Jan 2024 16:42:32 +0100 Subject: [PATCH 099/124] refactor linking loader --- src/pages/home/ReportScreen.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 2a62cdf43466..2fd3c4ed3f21 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -173,7 +173,6 @@ function ReportScreen({ const firstRenderRef = useRef(true); const reportIDFromRoute = getReportID(route); const reportActionIDFromRoute = lodashGet(route, 'params.reportActionID', null); - const shouldTriggerLoadingRef = useRef(!!reportActionIDFromRoute); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); @@ -187,6 +186,11 @@ function ReportScreen({ return _.filter(currentRangeOfReportActions, (reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); }, [reportActionIDFromRoute, allReportActions]); + // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. + useLayoutEffect(() => { + setLinkingToMessage(!!reportActionIDFromRoute); + }, [route, reportActionIDFromRoute]); + const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -197,12 +201,6 @@ function ReportScreen({ Performance.markStart(CONST.TIMING.CHAT_RENDER); } - // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. - useLayoutEffect(() => { - shouldTriggerLoadingRef.current = !!reportActionIDFromRoute; - setLinkingToMessage(!!reportActionIDFromRoute); - }, [route, reportActionIDFromRoute]); - const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -497,13 +495,9 @@ function ReportScreen({ // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. useEffect(() => { - if (reportMetadata.isLoadingInitialReportActions && shouldTriggerLoadingRef.current) { - shouldTriggerLoadingRef.current = false; - return; - } - if (!reportMetadata.isLoadingInitialReportActions && !shouldTriggerLoadingRef.current) { + InteractionManager.runAfterInteractions(() => { setLinkingToMessage(false); - } + }); }, [reportMetadata.isLoadingInitialReportActions]); const onLinkPress = () => { @@ -515,7 +509,7 @@ function ReportScreen({ if (!reportActionIDFromRoute) { return false; } - return ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); + return !_.isEmpty(allReportActions[reportActionIDFromRoute]) && ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); }, [reportActionIDFromRoute, allReportActions]); if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage)) { From c3d93ea4ef92087aafc186ffa1f4dd909aa98cb4 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 24 Jan 2024 16:46:43 +0100 Subject: [PATCH 100/124] hide loading indicator when delete --- src/pages/home/report/ReportActionsList.js | 9 +++++++-- src/pages/home/report/ReportActionsView.js | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 1b6de509000a..8bc26eea7d70 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -177,7 +177,9 @@ function ReportActionsList({ const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); - const isLastPendingActionIsAdd = lodashGet(sortedVisibleReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + const lastPendingAction = lodashGet(sortedReportActions, [0, 'pendingAction']) + const isLastPendingActionIsAdd = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + const isLastPendingActionIsDelete = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd; @@ -516,10 +518,13 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions, isOffline]); + // When performing comment linking, initially 25 items are added to the list. Subsequent fetches add 15 items from the cache or 50 items from the server. + // This is to ensure that the user is able to see the 'scroll to newer comments' button when they do comment linking and have not reached the end of the list yet. + const canScrollToNewerComments = !isLoadingInitialReportActions && !hasNewestReportAction && sortedReportActions.length > 25 && !isLastPendingActionIsDelete; return ( <> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 00584859c4f9..8b674b9e05a9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -337,7 +337,12 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const loadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { - if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { + if ( + props.isLoadingInitialReportActions || + props.isLoadingOlderReportActions || + props.network.isOffline || + newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ) { return; } // Determines if loading older reports is necessary when the content is smaller than the list @@ -362,6 +367,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { firstReportActionID, props.network.isOffline, reportActions.length, + newestReportAction, ], ); From 1d4aef156bcac0759cb1c5dbd51189071dffb6c8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 25 Jan 2024 11:43:54 +0100 Subject: [PATCH 101/124] fix scrolling to the bottom on action deletion from the same account on a different device --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 1 + src/pages/home/report/ReportActionsList.js | 21 +++++++------------ src/pages/home/report/ReportActionsView.js | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e007f63c8e97..d83e54f74d66 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -39,3 +39,4 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa BaseInvertedFlatList.displayName = 'BaseInvertedFlatList'; export default forwardRef(BaseInvertedFlatList); +export {AUTOSCROLL_TO_TOP_THRESHOLD}; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 8bc26eea7d70..3a397b4f6cf6 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -6,6 +6,7 @@ import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; +import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; import {withPersonalDetails} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; @@ -177,12 +178,7 @@ function ReportActionsList({ const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); - const lastPendingAction = lodashGet(sortedReportActions, [0, 'pendingAction']) - const isLastPendingActionIsAdd = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; - const isLastPendingActionIsDelete = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - - // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. - const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd; + const isLastPendingActionIsDelete = lodashGet(sortedReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // 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 @@ -198,17 +194,16 @@ function ReportActionsList({ useEffect(() => { if ( - (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) || - isNewestActionAvailableAndPendingAdd + scrollingVerticalOffset.current < AUTOSCROLL_TO_TOP_THRESHOLD && + previousLastIndex.current !== lastActionIndex && + reportActionSize.current > sortedVisibleReportActions.length && + hasNewestReportAction ) { - // runAfterInteractions is used for isNewestActionAvailableAndPendingAdd - InteractionManager.runAfterInteractions(() => { - reportScrollManager.scrollToBottom(); - }); + reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, isLastPendingActionIsAdd, linkedReportActionID, isNewestActionAvailableAndPendingAdd]); + }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, linkedReportActionID]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8b674b9e05a9..eb6c7634f536 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -426,7 +426,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { ]); // Check if the first report action in the list is the one we're currently linked to - const isTheFirstReportActionIsLinked = firstReportActionID !== reportActionID; + const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { if (isTheFirstReportActionIsLinked) { @@ -444,7 +444,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { return null; } // AutoScroll is disabled when we do linking to a specific reportAction - const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || isInitialLinkedView); + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isInitialLinkedView); return ( <> From 4f4a2c21d112e39a41b2339acc0046893114a83c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 29 Jan 2024 12:14:43 +0100 Subject: [PATCH 102/124] move pagination size to getInitialPaginationSize --- src/CONST.ts | 3 +++ src/pages/home/report/ReportActionsList.js | 9 +++++---- src/pages/home/report/ReportActionsView.js | 5 +++-- .../getInitialPaginationSize/index.native.ts | 6 ++++++ .../home/report/getInitialPaginationSize/index.ts | 14 ++++++++++++++ 5 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/pages/home/report/getInitialPaginationSize/index.native.ts create mode 100644 src/pages/home/report/getInitialPaginationSize/index.ts diff --git a/src/CONST.ts b/src/CONST.ts index ff3934c31943..18806bd0cf59 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3164,6 +3164,9 @@ const CONST = { MINI_CONTEXT_MENU_MAX_ITEMS: 4, REPORT_FIELD_TITLE_FIELD_ID: 'text_title', + + MOBILE_PAGINATION_SIZE: 15, + WEB_PAGINATION_SIZE: 50 } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 4f2939c553ac..7f4d1228e17f 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -501,10 +501,11 @@ function ReportActionsList({ const extraData = [isSmallScreenWidth ? currentUnreadMarker : undefined, ReportUtils.isArchivedRoom(report)]; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; + const canShowHeader = !isOffline && !hasHeaderRendered.current && scrollingVerticalOffset.current > VERTICAL_OFFSET_THRESHOLD; const contentContainerStyle = useMemo( - () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], - [isLoadingNewerReportActions, styles.chatContentScrollView, styles.chatContentScrollViewWithHeaderLoader], + () => [styles.chatContentScrollView, isLoadingNewerReportActions && canShowHeader ? styles.chatContentScrollViewWithHeaderLoader : {}], + [isLoadingNewerReportActions, styles.chatContentScrollView, styles.chatContentScrollViewWithHeaderLoader, canShowHeader], ); const lastReportAction = useMemo(() => _.last(sortedReportActions) || {}, [sortedReportActions]); @@ -542,7 +543,7 @@ function ReportActionsList({ ); const listHeaderComponent = useCallback(() => { - if (!isOffline && !hasHeaderRendered.current) { + if (!canShowHeader) { hasHeaderRendered.current = true; return null; } @@ -553,7 +554,7 @@ function ReportActionsList({ isLoadingNewerReportActions={isLoadingNewerReportActions} /> ); - }, [isLoadingNewerReportActions, isOffline]); + }, [isLoadingNewerReportActions, canShowHeader]); // When performing comment linking, initially 25 items are added to the list. Subsequent fetches add 15 items from the cache or 50 items from the server. // This is to ensure that the user is able to see the 'scroll to newer comments' button when they do comment linking and have not reached the end of the list yet. diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ad0657818c18..ec227f21518c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -25,6 +25,7 @@ import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import getInitialPaginationSize from './getInitialPaginationSize'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import reportActionPropTypes from './reportActionPropTypes'; import ReportActionsList from './ReportActionsList'; @@ -84,7 +85,6 @@ const defaultProps = { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; -const PAGINATION_SIZE = 15; let listIDCount = Math.round(Math.random() * 100); @@ -138,7 +138,8 @@ const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerRepo if (isFirstLinkedActionRender.current) { return allReportActions.slice(index, allReportActions.length); } - const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; + const paginationSize = getInitialPaginationSize(allReportActions.length - index); + const newStartIndex = index >= paginationSize ? index - paginationSize : 0; return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; // currentReportActionID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/home/report/getInitialPaginationSize/index.native.ts b/src/pages/home/report/getInitialPaginationSize/index.native.ts new file mode 100644 index 000000000000..69dbf5025ac5 --- /dev/null +++ b/src/pages/home/report/getInitialPaginationSize/index.native.ts @@ -0,0 +1,6 @@ +import CONST from '@src/CONST'; + +function getInitialPaginationSize(): number { + return CONST.MOBILE_PAGINATION_SIZE; +} +export default getInitialPaginationSize; diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/pages/home/report/getInitialPaginationSize/index.ts new file mode 100644 index 000000000000..3ec971738977 --- /dev/null +++ b/src/pages/home/report/getInitialPaginationSize/index.ts @@ -0,0 +1,14 @@ +import * as Browser from '@libs/Browser'; +import CONST from '@src/CONST'; + +const isMobileChrome = Browser.isMobileChrome(); +const isMobileSafari = Browser.isMobileSafari(); + +function getInitialPaginationSize(numToRender: number): number { + if (isMobileChrome || isMobileSafari) { + return Math.round(Math.min(numToRender / 3, CONST.MOBILE_PAGINATION_SIZE)); + } + // WEB: Calculate and position it correctly for each frame, enabling the rendering of up to 50 items. + return CONST.WEB_PAGINATION_SIZE; +} +export default getInitialPaginationSize; From 48824e545c15ae0e42bac7d66b43e4b81a3c2b48 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 29 Jan 2024 12:50:43 +0100 Subject: [PATCH 103/124] adjust getInitialPaginationSize --- src/CONST.ts | 2 +- src/pages/home/report/getInitialPaginationSize/index.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 18806bd0cf59..a0696560dc56 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3166,7 +3166,7 @@ const CONST = { REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, - WEB_PAGINATION_SIZE: 50 + WEB_PAGINATION_SIZE: 50, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/pages/home/report/getInitialPaginationSize/index.ts index 3ec971738977..019354c02946 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.ts @@ -1,11 +1,10 @@ import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; -const isMobileChrome = Browser.isMobileChrome(); const isMobileSafari = Browser.isMobileSafari(); function getInitialPaginationSize(numToRender: number): number { - if (isMobileChrome || isMobileSafari) { + if (isMobileSafari) { return Math.round(Math.min(numToRender / 3, CONST.MOBILE_PAGINATION_SIZE)); } // WEB: Calculate and position it correctly for each frame, enabling the rendering of up to 50 items. From 1f900683593ebee873dc38ec9eb66fdc31e1c254 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 30 Jan 2024 11:10:03 +0100 Subject: [PATCH 104/124] update after merge --- src/libs/API/parameters/OpenReportParams.ts | 1 + src/pages/home/report/ReportActionsView.js | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/OpenReportParams.ts b/src/libs/API/parameters/OpenReportParams.ts index 477a002516de..8eaed6bc0fde 100644 --- a/src/libs/API/parameters/OpenReportParams.ts +++ b/src/libs/API/parameters/OpenReportParams.ts @@ -1,5 +1,6 @@ type OpenReportParams = { reportID: string; + reportActionID?: string; emailList?: string; accountIDList?: string; parentReportActionID?: string; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ec227f21518c..31656060c7f2 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -186,7 +186,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions), [props.reportActions]); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); - const [isInitialLinkedView, setIsInitialLinkedView] = useState(false); + const [isInitialLinkedView, setIsInitialLinkedView] = useState(!!reportActionID); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; @@ -430,14 +430,25 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { + let timerId; + if (isTheFirstReportActionIsLinked) { - // this should be applied after we navigated to linked reportAction + setIsInitialLinkedView(true); + } else { + // After navigating to the linked reportAction, apply this to correctly set + // `autoscrollToTopThreshold` prop when linking to a specific reportAction. InteractionManager.runAfterInteractions(() => { - setIsInitialLinkedView(true); + // Using a short delay to ensure the view is updated after interactions + timerId = setTimeout(() => setIsInitialLinkedView(false), 10); }); - } else { - setIsInitialLinkedView(false); } + + return () => { + if (!timerId) { + return; + } + clearTimeout(timerId); + }; }, [isTheFirstReportActionIsLinked]); // Comments have not loaded at all yet do nothing From 28eae1dd56a9d69ba0753c4794cfd1b88ef5225a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 1 Feb 2024 17:26:12 +0100 Subject: [PATCH 105/124] WIP handling whisperedToAccountIDs, INVITE_TO_ROOM, CLOSED, CREATED --- src/libs/ReportActionsUtils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 559994e2a172..c921a323b0e7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -237,7 +237,14 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? // Iterate forwards through the array, starting from endIndex. This loop checks the continuity of actions by: // 1. Comparing the current item's previousReportActionID with the next item's reportActionID. // This ensures that we are moving in a sequence of related actions from newer to older. - while (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) { + while ( + (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) || + sortedReportActions[endIndex + 1]?.whisperedToAccountIDs?.length || + sortedReportActions[endIndex]?.whisperedToAccountIDs?.length || + sortedReportActions[endIndex]?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || + sortedReportActions[endIndex + 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED || + sortedReportActions[endIndex + 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED + ) { endIndex++; } @@ -529,6 +536,7 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null, s let filteredReportActions; if (shouldIncludeInvisibleActions) { + // filteredReportActions = Object.values(reportActions ?? {}).filter((action) => action?.resolution !== CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE) filteredReportActions = Object.values(reportActions ?? {}); } else { filteredReportActions = Object.entries(reportActions ?? {}) From 150e9a4b3e1be297b4c19bc01eb9f5fac0c66c08 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 2 Feb 2024 10:55:38 +0100 Subject: [PATCH 106/124] lint --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 136c4a86c3c7..406c47b847af 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -278,7 +278,7 @@ function ReportScreen({ const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS_NUM.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); - const isLoading = !reportIDFromRoute || !isSidebarLoaded ||PersonalDetailsUtils.isPersonalDetailsEmpty(); + const isLoading = !reportIDFromRoute || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty(); const lastReportAction = useMemo( () => reportActions.length From 6f9746a40f8e0d91cc0e1b868c473f9183aa857e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 2 Feb 2024 13:33:33 +0100 Subject: [PATCH 107/124] Include 'INVITE_TO_ROOM' action for startIndex calculation --- src/libs/ReportActionsUtils.ts | 25 ++++++++++++------- src/pages/home/report/ReportActionsView.js | 2 +- .../report/getInitialPaginationSize/index.ts | 9 +------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index aa9438e5d964..9bc15794f03a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -42,6 +42,10 @@ type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMe const policyChangeActionsSet = new Set(Object.values(CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG)); const allReports: OnyxCollection = {}; + +type ActionableMentionWhisperResolution = { + resolution: ValueOf; +}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { @@ -241,8 +245,8 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? // This ensures that we are moving in a sequence of related actions from newer to older. while ( (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) || - sortedReportActions[endIndex + 1]?.whisperedToAccountIDs?.length || - sortedReportActions[endIndex]?.whisperedToAccountIDs?.length || + !!sortedReportActions[endIndex + 1]?.whisperedToAccountIDs?.length || + !!sortedReportActions[endIndex]?.whisperedToAccountIDs?.length || sortedReportActions[endIndex]?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || sortedReportActions[endIndex + 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED || sortedReportActions[endIndex + 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED @@ -257,7 +261,8 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? // This additional check is to include recently sent messages that might not yet be part of the established sequence. while ( (startIndex > 0 && sortedReportActions[startIndex].reportActionID === sortedReportActions[startIndex - 1].previousReportActionID) || - sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD + sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD || + sortedReportActions[startIndex - 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ) { startIndex--; } @@ -526,16 +531,18 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldIncludeInvisibleActions = false): ReportAction[] { - let filteredReportActions; +function getSortedReportActionsForDisplay(reportActions: ReportActions | null | ActionableMentionWhisperResolution, shouldIncludeInvisibleActions = false): ReportAction[] { + let filteredReportActions: ReportAction[] = []; + if (!reportActions) { + return []; + } if (shouldIncludeInvisibleActions) { - // filteredReportActions = Object.values(reportActions ?? {}).filter((action) => action?.resolution !== CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE) - filteredReportActions = Object.values(reportActions ?? {}); + filteredReportActions = Object.values(reportActions).filter((action): action is ReportAction => !action?.resolution); } else { - filteredReportActions = Object.entries(reportActions ?? {}) + filteredReportActions = Object.entries(reportActions) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) - .map((entry) => entry[1]); + .map(([, reportAction]) => reportAction as ReportAction); } const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURLInPolicyChangeLogAction(reportAction)); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8e49f3fffeb4..0af4f17c8686 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -138,7 +138,7 @@ const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerRepo if (isFirstLinkedActionRender.current) { return allReportActions.slice(index, allReportActions.length); } - const paginationSize = getInitialPaginationSize(allReportActions.length - index); + const paginationSize = getInitialPaginationSize(); const newStartIndex = index >= paginationSize ? index - paginationSize : 0; return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; // currentReportActionID is needed to trigger batching once the report action has been positioned diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/pages/home/report/getInitialPaginationSize/index.ts index 019354c02946..d1467c0325b7 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.ts @@ -1,13 +1,6 @@ -import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; -const isMobileSafari = Browser.isMobileSafari(); - -function getInitialPaginationSize(numToRender: number): number { - if (isMobileSafari) { - return Math.round(Math.min(numToRender / 3, CONST.MOBILE_PAGINATION_SIZE)); - } - // WEB: Calculate and position it correctly for each frame, enabling the rendering of up to 50 items. +function getInitialPaginationSize(): number { return CONST.WEB_PAGINATION_SIZE; } export default getInitialPaginationSize; From b2acf1d146e127c39e398e7598619347ac3ae487 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 2 Feb 2024 16:17:16 +0100 Subject: [PATCH 108/124] fix after merge (add allReportActions to memo) --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 406c47b847af..888b05c1bed7 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -708,7 +708,7 @@ export default compose( ReportScreen, (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - _.isEqual(prevProps.reportActions, nextProps.reportActions) && + _.isEqual(prevProps.allReportActions, nextProps.allReportActions) && _.isEqual(prevProps.reportMetadata, nextProps.reportMetadata) && prevProps.isComposerFullSize === nextProps.isComposerFullSize && _.isEqual(prevProps.betas, nextProps.betas) && From 17c4fba70c1d4f8a9aaebd8e7a7c045227ba83ab Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 7 Feb 2024 11:27:38 +0100 Subject: [PATCH 109/124] comment out DeleteWorkspace --- src/libs/actions/Policy.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 866206895d5e..41217cee970c 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -434,13 +434,14 @@ function removeMembers(accountIDs: number[], policyID: string) { }, }); }); - optimisticClosedReportActions.forEach((reportAction, index) => { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${workspaceChats?.[index]?.reportID}`, - value: {[reportAction.reportActionID]: reportAction as ReportAction}, - }); - }); + // comment out for time this issue would be resolved https://github.com/Expensify/App/issues/35952 + // optimisticClosedReportActions.forEach((reportAction, index) => { + // optimisticData.push({ + // onyxMethod: Onyx.METHOD.MERGE, + // key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${workspaceChats?.[index]?.reportID}`, + // value: {[reportAction.reportActionID]: reportAction as ReportAction}, + // }); + // }); // If the policy has primaryLoginsInvited, then it displays informative messages on the members page about which primary logins were added by secondary logins. // If we delete all these logins then we should clear the informative messages since they are no longer relevant. From 73c48aa99b3ddf18561e02395478124cced0fc85 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 7 Feb 2024 14:40:21 +0100 Subject: [PATCH 110/124] add route to memo --- src/components/FlatList/MVCPFlatList.js | 2 +- src/pages/home/ReportScreen.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 1b6bca14ecf3..c815774eeabd 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -51,7 +51,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont const scrollToOffset = React.useCallback( (offset, animated) => { const behavior = animated ? 'smooth' : 'instant'; - scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); + scrollRef.current?.getScrollableNode()?.scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); }, [horizontal], ); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 116ca0f41762..6609d06e0c63 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -719,6 +719,7 @@ export default compose( prevProps.isComposerFullSize === nextProps.isComposerFullSize && _.isEqual(prevProps.betas, nextProps.betas) && _.isEqual(prevProps.policies, nextProps.policies) && + _.isEqual(prevProps.route, nextProps.route) && prevProps.accountManagerReportID === nextProps.accountManagerReportID && prevProps.userLeavingStatus === nextProps.userLeavingStatus && prevProps.currentReportID === nextProps.currentReportID && From a83cf6ecf91eeab3df13c1c7db4837172bb47714 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 15:26:05 +0100 Subject: [PATCH 111/124] undo https://github.com/Expensify/App/pull/37839/files --- src/libs/ReportActionsUtils.ts | 1 - src/libs/actions/Report.ts | 28 +++-------- src/pages/home/ReportScreen.tsx | 2 +- src/pages/home/report/ReportActionsList.tsx | 10 +++- src/pages/home/report/ReportActionsView.tsx | 56 ++++++++++----------- 5 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 064c35fda0b6..b8783073a407 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -261,7 +261,6 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; - console.log('get.sortedReportActions.0', sortedReportActions); if (id) { index = sortedReportActions.findIndex((obj) => obj.reportActionID === id); } else { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 375888fd6e9c..06958c5ddaf7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -116,28 +116,14 @@ Onyx.connect({ // map of reportID to all reportActions for that report const allReportActions: OnyxCollection = {}; -// map of reportID to the ID of the oldest reportAction for that report -const oldestReportActions: Record = {}; - -// map of report to the ID of the newest action for that report -const newestReportActions: Record = {}; - Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + callback: (action, key) => { + if (!key || !action) { return; } const reportID = CollectionUtils.extractCollectionItemID(key); - allReportActions[reportID] = actions; - const sortedActions = ReportActionsUtils.getSortedReportActions(Object.values(actions)); - - if (sortedActions.length === 0) { - return; - } - - oldestReportActions[reportID] = sortedActions[0].reportActionID; - newestReportActions[reportID] = sortedActions[sortedActions.length - 1].reportActionID; + allReportActions[reportID] = action; }, }); @@ -898,7 +884,7 @@ function reconnect(reportID: string) { * Gets the older actions that have not been read yet. * Normally happens when you scroll up on a chat, and the actions have not been read yet. */ -function getOlderActions(reportID: string) { +function getOlderActions(reportID: string, reportActionID: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -931,7 +917,7 @@ function getOlderActions(reportID: string) { const parameters: GetOlderActionsParams = { reportID, - reportActionID: oldestReportActions[reportID], + reportActionID, }; API.read(READ_COMMANDS.GET_OLDER_ACTIONS, parameters, {optimisticData, successData, failureData}); @@ -941,7 +927,7 @@ function getOlderActions(reportID: string) { * Gets the newer actions that have not been read yet. * Normally happens when you are not located at the bottom of the list and scroll down on a chat. */ -function getNewerActions(reportID: string) { +function getNewerActions(reportID: string, reportActionID: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -974,7 +960,7 @@ function getNewerActions(reportID: string) { const parameters: GetNewerActionsParams = { reportID, - reportActionID: newestReportActions[reportID], + reportActionID, }; API.read(READ_COMMANDS.GET_NEWER_ACTIONS, parameters, {optimisticData, successData, failureData}); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 6ce05ac2e9ea..ce46c0b2a0cd 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -232,7 +232,7 @@ function ReportScreen({ } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); - return currentRangeOfReportActions.filter((reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); + return currentRangeOfReportActions; }, [reportActionIDFromRoute, allReportActions]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 542255ce6982..fcfcb912dc22 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -167,7 +167,12 @@ function ReportActionsList({ const lastReadTimeRef = useRef(report.lastReadTime); const sortedVisibleReportActions = useMemo( - () => sortedReportActions.filter((reportAction) => isOffline || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors), + () => + sortedReportActions.filter( + (reportAction) => + (isOffline || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && + ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID), + ), [sortedReportActions, isOffline], ); const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; @@ -575,7 +580,8 @@ function ReportActionsList({ ref={reportScrollManager.ref} testID="report-actions-list" style={styles.overscrollBehaviorContain} - data={sortedReportActions} + // data={sortedReportActions} + data={sortedVisibleReportActions} renderItem={renderItem} contentContainerStyle={contentContainerStyle} keyExtractor={keyExtractor} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 7a6e28a59d23..3cdaea4aaea6 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,8 +1,8 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; +import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; -import lodashIsEqual from 'lodash/isEqual'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; @@ -56,21 +56,28 @@ const SPACER = 16; let listIDCount = Math.round(Math.random() * 100); -// /** -// * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. -// * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, -// * and manages pagination through 'handleReportActionPagination' function. -// * -// * @param {string} linkedID - ID of the linked message used for initial focus. -// * @param {array} allReportActions - Array of messages. -// * @param {function} fetchNewerReportActions - Function to fetch more messages. -// * @param {string} route - Current route, used to reset states on route change. -// * @param {boolean} isLoading - Loading state indicator. -// * @param {boolean} triggerListID - Used to trigger a listID change. -// * @returns {object} An object containing the sliced message array, the pagination function, -// * index of the linked message, and a unique list ID. -// */ -const usePaginatedReportActionList = (linkedID:string, allReportActions: OnyxTypes.ReportAction[], fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, route: string, isLoading: boolean, triggerListID: boolean) => { +/** + * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, + * and manages pagination through 'handleReportActionPagination' function. + * + * linkedID - ID of the linked message used for initial focus. + * allReportActions - Array of messages. + * fetchNewerReportActions - Function to fetch more messages. + * route - Current route, used to reset states on route change. + * isLoading - Loading state indicator. + * triggerListID - Used to trigger a listID change. + * returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +const usePaginatedReportActionList = ( + linkedID: string, + allReportActions: OnyxTypes.ReportAction[], + fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, + route: string, + isLoading: boolean, + triggerListID: boolean, +) => { // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned const [currentReportActionID, setCurrentReportActionID] = useState(''); @@ -139,7 +146,6 @@ const usePaginatedReportActionList = (linkedID:string, allReportActions: OnyxTy }; }; - function ReportActionsView({ report, session, @@ -150,16 +156,15 @@ function ReportActionsView({ isLoadingNewerReportActions = false, isReadyForCommentLinking = false, }: ReportActionsViewProps) { - useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); - const reportActionID = lodashGet(route, 'params.reportActionID', null); + const reportActionID = route?.params?.reportActionID ?? null; const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); -const network = useNetwork(); -const {isSmallScreenWidth} = useWindowDimensions(); + const network = useNetwork(); + const {isSmallScreenWidth} = useWindowDimensions(); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); const {windowHeight} = useWindowDimensions(); @@ -317,12 +322,7 @@ const {isSmallScreenWidth} = useWindowDimensions(); const loadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { - if ( - isLoadingInitialReportActions || - isLoadingOlderReportActions || - network.isOffline || - newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE - ) { + if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } // Determines if loading older reports is necessary when the content is smaller than the list @@ -464,7 +464,7 @@ const {isSmallScreenWidth} = useWindowDimensions(); ReportActionsView.displayName = 'ReportActionsView'; ReportActionsView.initMeasured = false; - function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActionsViewProps): boolean { +function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActionsViewProps): boolean { if (!lodashIsEqual(oldProps.isReadyForCommentLinking, newProps.isReadyForCommentLinking)) { return false; } From 173c11b1298fd93af604898bb83abcccf5f83b7e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 15:26:34 +0100 Subject: [PATCH 112/124] sync package lock --- package-lock.json | 147 ---------------------------------------------- 1 file changed, 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc373abcd9b0..5cf96414d5f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8045,153 +8045,6 @@ "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", "license": "MIT" }, - "node_modules/@onfido/active-video-capture": { - "version": "0.28.6", - "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz", - "integrity": "sha512-RFUeKaOSjj/amPp6VzhVkq/7kIkutEnnttT9n5KDeD3Vx8a09KD3a/xvxdQppveHlDAYsdBP6LrJwSSpjXiprg==", - "dependencies": { - "@mediapipe/face_detection": "^0.4.1646425229", - "@mediapipe/face_mesh": "^0.4.1633559619", - "@onfido/castor": "^2.2.2", - "@onfido/castor-icons": "^2.12.0", - "@tensorflow-models/face-detection": "^1.0.1", - "@tensorflow-models/face-landmarks-detection": "^1.0.2", - "@tensorflow/tfjs-backend-wasm": "3.20.0", - "@tensorflow/tfjs-backend-webgl": "3.20.0", - "@tensorflow/tfjs-converter": "3.20.0", - "@tensorflow/tfjs-core": "3.20.0", - "preact": "10.11.3", - "react-webcam": "^7.2.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow-models/face-landmarks-detection": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@tensorflow-models/face-landmarks-detection/-/face-landmarks-detection-1.0.5.tgz", - "integrity": "sha512-54XJPi8g29/MknJ33ZBrLsEzr9kw/dJtrJMMD3xrCrnRlfFQPIKQ5PI2Wml55Fz2p4U2hemzBB0/H+S94JddIQ==", - "dependencies": { - "rimraf": "^3.0.2" - }, - "peerDependencies": { - "@mediapipe/face_mesh": "~0.4.0", - "@tensorflow-models/face-detection": "~1.0.0", - "@tensorflow/tfjs-backend-webgl": "^3.12.0", - "@tensorflow/tfjs-converter": "^3.12.0", - "@tensorflow/tfjs-core": "^3.12.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.20.0.tgz", - "integrity": "sha512-gf075YaBLwSAAiUwa0D4GvYyUBhbJ1BVSivUNQmUfGKvIr2lIhF0qstBr033YTc3lhkbFSHEEPAHh/EfpqyjXQ==", - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-wasm": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-wasm/-/tfjs-backend-wasm-3.20.0.tgz", - "integrity": "sha512-k+sDcrcPtGToLjKRffgtSqlcN4MC6g4hXWRarZfgvvyvFqpxVfVqrGYHGTirXdN47sKYhmcTSMvbM2quGaaQnA==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/emscripten": "~0.0.34" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.20.0.tgz", - "integrity": "sha512-SucbyQ08re3HvRgVfarRtKFIjNM4JvIAzcXmw4vaE/HrCtPEePkGO1VrmfQoN470EdUmGiwgqAjoyBvM2VOlVg==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@types/webgl2": "0.0.6", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-converter": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.20.0.tgz", - "integrity": "sha512-8EIYqtQwvSYw9GFNW2OFU8Qnl/FQF/kKAsQJoORYaZ419WJo+FIZWbAWDtCpJSAgkgoHH1jYWgV9H313cVmqxg==", - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-core": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.20.0.tgz", - "integrity": "sha512-L16JyVA4a8jFJXFgB9/oYZxcGq/GfLypt5dMVTyedznARZZ9SiY/UMMbo3IKl9ZylG1dOVVTpjzV3EvBYfeJXw==", - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@webgpu/types": "0.1.16", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@webgpu/types": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", - "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==" - }, - "node_modules/@onfido/castor": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@onfido/castor/-/castor-2.3.0.tgz", - "integrity": "sha512-FkydkjedS6b2g3SqgZMYnVRZvUs/MkaEuXXJWG9+LNc7DMFT1K8smOnNuHzkiM3cJhXL6yAADdKE0mg+ZIrucQ==", - "dependencies": { - "@onfido/castor-tokens": "^1.0.0-beta.6", - "csstype": "^3.1.1" - }, - "peerDependencies": { - "@onfido/castor-icons": ">=1.0.0" - } - }, - "node_modules/@onfido/castor-icons": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@onfido/castor-icons/-/castor-icons-2.22.0.tgz", - "integrity": "sha512-7OnCvu5xqVWcBLqovZyb99NP0oHw7sjkVYXZhi438i0U6Pgecrhu/14Gc/IN/kvgDxWj9qmiYdd0qdjNaVckrQ==", - "peerDependencies": { - "react": ">=17 || ^16.14 || ^15.7 || ^0.14.10" - } - }, - "node_modules/@onfido/castor-tokens": { - "version": "1.0.0-beta.6", - "resolved": "https://registry.npmjs.org/@onfido/castor-tokens/-/castor-tokens-1.0.0-beta.6.tgz", - "integrity": "sha512-MfwuSlNdM0Ay0cI3LLyqZGsHW0e1Y1R/0IdQKVU575PdWQx1Q/538aOZMo/a3/oSW0pMEgfOm+mNqPx057cvWA==" - }, - "node_modules/@onfido/opencv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@onfido/opencv/-/opencv-2.1.1.tgz", - "integrity": "sha512-Bwo0YsZrrdm+p5hpNFZ7yrqNVWJxOUbQW9aWDEUtkDWUL+nX2RHIR6F4lBGVmbqnG24anadS/+nEvy80SwD3tQ==", - "dependencies": { - "mirada": "^0.0.15" - } - }, "node_modules/@onfido/react-native-sdk": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-10.6.0.tgz", From 4461124adcf25444c2d552ab5a021840a01cbb75 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 17:27:53 +0100 Subject: [PATCH 113/124] migrate to TS --- .../ReportActionItem/MoneyRequestAction.tsx | 12 ------- src/pages/home/ReportScreen.tsx | 29 ++++++++------- src/pages/home/report/ReportActionsList.tsx | 12 +++++-- src/pages/home/report/ReportActionsView.tsx | 35 +++++++++++-------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index a6c7754b0ce9..05891311ba6d 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -89,19 +89,7 @@ function MoneyRequestAction({ return; } -// <<<<<<< HEAD -// // If the childReportID is not present, we need to create a new thread -// const childReportID = action?.childReportID; -// if (!childReportID) { -// const thread = ReportUtils.buildTransactionThread(action, requestReportID); -// const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []); -// Report.openReport(thread.reportID, '', userLogins, thread, action.reportActionID); -// Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); -// return; -// } -// ======= const childReportID = action?.childReportID ?? '0'; -// >>>>>>> da7697734c1f759786eba0a643a062b4e39a47ad Report.openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index ce46c0b2a0cd..b1138cd8a28e 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -1,8 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import lodashGet from 'lodash/get'; import lodashIsEqual from 'lodash/isEqual'; -import PropTypes from 'prop-types'; import React, {memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {FlatList, ViewStyle} from 'react-native'; @@ -55,9 +53,11 @@ import ReportFooter from './report/ReportFooter'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; +type ReportActionMap = Record; + type ReportScreenOnyxProps = { /** All the report actions for this report */ - allReportActions: OnyxTypes.ReportAction[]; + allReportActions: ReportActionMap; /** Tells us if the sidebar has rendered */ isSidebarLoaded: OnyxEntry; @@ -126,13 +126,12 @@ function ReportScreen({ userLeavingStatus = false, currentReportID = '', navigation, - errors, }: ReportScreenProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const reportIDFromRoute = getReportID(route); - const reportActionIDFromRoute = lodashGet(route, 'params.reportActionID', null); + const reportActionIDFromRoute = route?.params?.reportActionID ?? ''; const isFocused = useIsFocused(); const firstRenderRef = useRef(true); @@ -227,7 +226,7 @@ function ReportScreen({ const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { - if (_.isEmpty(allReportActions)) { + if (!allReportActions) { return []; } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); @@ -315,11 +314,11 @@ function ReportScreen({ const isReportReadyForDisplay = useMemo((): boolean => { // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely const isTransitioning = report && report.reportID !== reportIDFromRoute; - return reportIDFromRoute !== '' && report.reportID && !isTransitioning; + return reportIDFromRoute !== '' && !!report.reportID && !isTransitioning; }, [report, reportIDFromRoute]); const shouldShowSkeleton = - isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); + isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata?.isLoadingInitialReportActions); const shouldShowReportActionList = isReportReadyForDisplay && !isLoading; @@ -503,7 +502,7 @@ function ReportScreen({ InteractionManager.runAfterInteractions(() => { setLinkingToMessage(false); }); - }, [reportMetadata.isLoadingInitialReportActions]); + }, [reportMetadata?.isLoadingInitialReportActions]); const onLinkPress = () => { Navigation.setParams({reportActionID: ''}); @@ -511,13 +510,14 @@ function ReportScreen({ }; const isLinkedReportActionDeleted = useMemo(() => { - if (!reportActionIDFromRoute) { + if (!reportActionIDFromRoute || !allReportActions) { return false; } - return !_.isEmpty(allReportActions[reportActionIDFromRoute]) && ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); + const action = allReportActions[reportActionIDFromRoute]; + return action && ReportActionsUtils.isDeletedAction(action); }, [reportActionIDFromRoute, allReportActions]); - if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage)) { + if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { return ( )} @@ -692,6 +690,7 @@ export default withViewportOffsetTop( prevProps.currentReportID === nextProps.currentReportID && prevProps.viewportOffsetTop === nextProps.viewportOffsetTop && lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && + lodashIsEqual(prevProps.route, nextProps.route) && lodashIsEqual(prevProps.report, nextProps.report), ), ), diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index fcfcb912dc22..4cc1f0e3720b 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -69,10 +69,19 @@ type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & { loadOlderChats: () => void; /** Function to load newer chats */ - loadNewerChats: LoadNewerChats; + loadNewerChats: () => void; /** Whether the composer is in full size */ isComposerFullSize?: boolean; + + /** ID of the list */ + listID: number; + + /** Callback executed on content size change */ + onContentSizeChange: (w: number, h: number) => void; + + /** Should enable auto scroll to top threshold */ + shouldEnableAutoScrollToTopThreshold: boolean; }; const VERTICAL_OFFSET_THRESHOLD = 200; @@ -580,7 +589,6 @@ function ReportActionsList({ ref={reportScrollManager.ref} testID="report-actions-list" style={styles.overscrollBehaviorContain} - // data={sortedReportActions} data={sortedVisibleReportActions} renderItem={renderItem} contentContainerStyle={contentContainerStyle} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 3cdaea4aaea6..403855fede39 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,8 +1,9 @@ +import type {RouteProp} from '@react-navigation/native'; import {useIsFocused, useRoute} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; @@ -11,6 +12,7 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; @@ -20,6 +22,7 @@ import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getInitialPaginationSize from './getInitialPaginationSize'; @@ -49,6 +52,9 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { /** The report actions are loading newer data */ isLoadingNewerReportActions?: boolean; + + /** Whether the report is ready for comment linking */ + isReadyForCommentLinking?: boolean; }; const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; @@ -74,7 +80,7 @@ const usePaginatedReportActionList = ( linkedID: string, allReportActions: OnyxTypes.ReportAction[], fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: string, + route: RouteProp, isLoading: boolean, triggerListID: boolean, ) => { @@ -124,7 +130,7 @@ const usePaginatedReportActionList = ( const newestReportAction = visibleReportActions?.[0]; const handleReportActionPagination = useCallback( - ({firstReportActionID}) => { + ({firstReportActionID}: {firstReportActionID: string}) => { // This function is a placeholder as the actual pagination is handled by visibleReportActions if (!hasMoreCached) { isFirstLinkedActionRender.current = false; @@ -158,8 +164,8 @@ function ReportActionsView({ }: ReportActionsViewProps) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); - const route = useRoute(); - const reportActionID = route?.params?.reportActionID ?? null; + const route = useRoute>(); + const reportActionID = route?.params?.reportActionID; const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); @@ -169,7 +175,6 @@ function ReportActionsView({ const layoutListHeight = useRef(0); const {windowHeight} = useWindowDimensions(); const isFocused = useIsFocused(); - const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const prevNetworkRef = useRef(network); const prevAuthTokenType = usePrevious(session?.authTokenType); const [isInitialLinkedView, setIsInitialLinkedView] = useState(!!reportActionID); @@ -182,7 +187,7 @@ function ReportActionsView({ * displaying. */ const fetchNewerAction = useCallback( - (newestReportAction) => { + (newestReportAction: OnyxTypes.ReportAction) => { if (isLoadingNewerReportActions || isLoadingInitialReportActions) { return; } @@ -198,12 +203,13 @@ function ReportActionsView({ linkedIdIndex, listID, } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); + const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActions = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; - const newestReportAction = lodashGet(reportActions, ['0']); + const newestReportAction = reportActions?.[0]; const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); - const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; - const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); + const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; + const firstReportActionName = reportActions?.[0]?.actionName; const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); @@ -293,7 +299,7 @@ function ReportActionsView({ } }, [report.pendingFields, didSubscribeToReportTypingEvents, reportID]); - const onContentSizeChange = useCallback((w, h) => { + const onContentSizeChange = useCallback((w: number, h: number) => { contentListHeight.current = h; }, []); @@ -355,7 +361,7 @@ function ReportActionsView({ * Runs when the FlatList finishes laying out */ const recordTimeToMeasureItemLayout = useCallback( - (e) => { + (e: LayoutChangeEvent) => { layoutListHeight.current = e.nativeEvent.layout.height; if (didLayout.current) { @@ -409,7 +415,7 @@ function ReportActionsView({ const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { - let timerId; + let timerId: NodeJS.Timeout; if (isTheFirstReportActionIsLinked) { setIsInitialLinkedView(true); @@ -447,11 +453,10 @@ function ReportActionsView({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} - isLinkingLoader={!!reportActionID && isLoadingInitialReportActions} + // isLinkingLoader={!!reportActionID && isLoadingInitialReportActions} isLoadingInitialReportActions={isLoadingInitialReportActions} isLoadingOlderReportActions={isLoadingOlderReportActions} isLoadingNewerReportActions={isLoadingNewerReportActions} - // policy={policy} listID={listID} onContentSizeChange={onContentSizeChange} shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll} From 27608f1e15579216c3a783756001b39c7b178fde Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 19:21:47 +0100 Subject: [PATCH 114/124] update tests --- tests/unit/ReportActionsUtilsTest.ts | 1396 ++++++++++++++++++++++++-- 1 file changed, 1298 insertions(+), 98 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 7a48bb73d336..aac8d445aa92 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -553,76 +553,732 @@ describe('ReportActionsUtils', () => { }); describe('getContinuousReportActionChain', () => { it('given an input ID of 1, ..., 7 it will return the report actions with id 1 - 7', () => { - const input = [ - // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, - - // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, - - // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, - ]; - - const expectedResult = [ - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 3); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { - const input = [ + const input: ReportAction[] = [ // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; const expectedResult = [ - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '3'); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + + // Note: another gap + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + + const expectedResult = [ + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 10); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '10'); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); @@ -630,66 +1286,610 @@ describe('ReportActionsUtils', () => { it('given an input ID of 14, ..., 17 it will return the report actions with id 14 - 17', () => { const input = [ // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; const expectedResult = [ - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 16); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '16'); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); it('given an input ID of 8 or 13 which are not exist in Onyx it will return an empty array', () => { - const input = [ + const input: ReportAction[] = [ // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; - const expectedResult = []; + const expectedResult: ReportAction[] = []; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '8'); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); From a0e96e1feb9714a0af77fae7481613b1f68ddc3d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 19:22:48 +0100 Subject: [PATCH 115/124] fix type --- src/components/InvertedFlatList/index.tsx | 6 +++++- src/libs/ReportActionsUtils.ts | 7 +++---- src/libs/migrateOnyx.ts | 8 +++++++- src/pages/home/report/ReportActionsList.tsx | 8 ++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/index.tsx b/src/components/InvertedFlatList/index.tsx index 2b4d98733cc4..37ca3c6201b5 100644 --- a/src/components/InvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/index.tsx @@ -6,9 +6,13 @@ import CONST from '@src/CONST'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import CellRendererComponent from './CellRendererComponent'; +type InvertedFlatListProps = FlatListProps & { + shouldEnableAutoScrollToTopThreshold?: boolean; +}; + // This is adapted from https://codesandbox.io/s/react-native-dsyse // It's a HACK alert since FlatList has inverted scrolling on web -function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: FlatListProps, ref: ForwardedRef) { +function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: InvertedFlatListProps, ref: ForwardedRef) { const lastScrollEvent = useRef(null); const scrollEndTimeout = useRef(null); const updateInProgress = useRef(false); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b8783073a407..f67878a71fe0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -52,9 +52,6 @@ const policyChangeActionsSet = new Set(Object.values(CONST.REPORT.ACTION const allReports: OnyxCollection = {}; -type ActionableMentionWhisperResolution = { - resolution: ValueOf; -}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { @@ -578,7 +575,9 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null | } if (shouldIncludeInvisibleActions) { - filteredReportActions = Object.values(reportActions).filter((action): action is ReportAction => !action?.resolution); + filteredReportActions = Object.values(reportActions).filter( + (action): action is ReportAction => !(action?.originalMessage as OriginalMessageActionableMentionWhisper['originalMessage'])?.resolution, + ); } else { filteredReportActions = Object.entries(reportActions) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index 58a2eebbb76a..536b912e69e0 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -11,7 +11,13 @@ export default function (): Promise { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [CheckForPreviousReportActionID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [ + CheckForPreviousReportActionID, + RenameReceiptFilename, + KeyReportActionsDraftByReportActionID, + TransactionBackupsToCollection, + RemoveEmptyReportActionsDrafts, + ]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 4cc1f0e3720b..8419df479bef 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -81,7 +81,7 @@ type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & { onContentSizeChange: (w: number, h: number) => void; /** Should enable auto scroll to top threshold */ - shouldEnableAutoScrollToTopThreshold: boolean; + shouldEnableAutoScrollToTopThreshold?: boolean; }; const VERTICAL_OFFSET_THRESHOLD = 200; @@ -300,7 +300,7 @@ function ReportActionsList({ }, []); const scrollToBottomForCurrentUserAction = useCallback( - (isFromCurrentUser) => { + (isFromCurrentUser: boolean) => { // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where // they are now in the list. if (!isFromCurrentUser || !hasNewestReportAction) { @@ -554,7 +554,7 @@ function ReportActionsList({ [onLayout], ); const onContentSizeChangeInner = useCallback( - (w, h) => { + (w: number, h: number) => { onContentSizeChange(w, h); }, [onContentSizeChange], @@ -618,4 +618,4 @@ ReportActionsList.displayName = 'ReportActionsList'; export default withCurrentUserPersonalDetails(memo(ReportActionsList)); -export type {LoadNewerChats}; +export type {LoadNewerChats, ReportActionsListProps}; From ce55a7e93d6c1a90ec07e75b1796d402488506b6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 13 Mar 2024 09:01:31 +0100 Subject: [PATCH 116/124] remove outdated test --- tests/unit/ReportActionsUtilsTest.ts | 110 --------------------------- 1 file changed, 110 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index aac8d445aa92..bf528eca3e81 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -305,116 +305,6 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); - describe('getSortedReportActionsForDisplay with marked the first reportAction', () => { - it('should filter out non-whitelisted actions', () => { - const input: ReportAction[] = [ - { - created: '2022-11-13 22:27:01.825', - reportActionID: '8401445780099176', - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-12 22:27:01.825', - reportActionID: '6401435781022176', - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-11 22:27:01.825', - reportActionID: '2962390724708756', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - originalMessage: { - amount: 0, - currency: 'USD', - type: 'split', // change to const - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-10 22:27:01.825', - reportActionID: '1609646094152486', - actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, - originalMessage: { - html: 'Hello world', - lastModified: '2022-11-10 22:27:01.825', - oldName: 'old name', - newName: 'new name', - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-09 22:27:01.825', - reportActionID: '8049485084562457', - actionName: CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.UPDATE_FIELD, - originalMessage: {}, - message: [{html: 'updated the Approval Mode from "Submit and Approve" to "Submit and Close"', type: 'Action type', text: 'Action text'}], - }, - { - created: '2022-11-08 22:27:06.825', - reportActionID: '1661970171066216', - actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED, - originalMessage: { - paymentType: 'ACH', - }, - message: [{html: 'Waiting for the bank account', type: 'Action type', text: 'Action text'}], - }, - { - created: '2022-11-06 22:27:08.825', - reportActionID: '1661970171066220', - actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [{html: 'I have changed the task', type: 'Action type', text: 'Action text'}], - }, - ]; - - const resultWithoutNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input); - const resultWithNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); - input.pop(); - // Mark the newest report action as the newest report action - resultWithoutNewestFlag[0] = { - ...resultWithoutNewestFlag[0], - isNewestReportAction: true, - }; - expect(resultWithoutNewestFlag).toStrictEqual(resultWithNewestFlag); - }); - }); - it('should filter out closed actions', () => { const input: ReportAction[] = [ { From 2b77edc3fa7ea5372fea151a7e2f326fcf02bf8b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 13 Mar 2024 09:08:38 +0100 Subject: [PATCH 117/124] add selector for sortedAllReportActions --- src/pages/home/ReportScreen.tsx | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b1138cd8a28e..6bd07f044dba 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -53,12 +53,7 @@ import ReportFooter from './report/ReportFooter'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; -type ReportActionMap = Record; - type ReportScreenOnyxProps = { - /** All the report actions for this report */ - allReportActions: ReportActionMap; - /** Tells us if the sidebar has rendered */ isSidebarLoaded: OnyxEntry; @@ -77,8 +72,8 @@ type ReportScreenOnyxProps = { /** Whether the composer is full size */ isComposerFullSize: OnyxEntry; - /** All the report actions for this report */ - // reportActions: OnyxTypes.ReportAction[]; + /** An array containing all report actions related to this report, sorted based on a date criterion */ + sortedAllReportActions: OnyxTypes.ReportAction[]; /** The report currently being looked at */ report: OnyxEntry; @@ -110,7 +105,7 @@ function ReportScreen({ betas = [], route, report: reportProp, - allReportActions, + sortedAllReportActions, reportMetadata = { isLoadingInitialReportActions: true, isLoadingOlderReportActions: false, @@ -226,13 +221,12 @@ function ReportScreen({ const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { - if (!allReportActions) { + if (!sortedAllReportActions.length) { return []; } - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); - const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); + const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionIDFromRoute); return currentRangeOfReportActions; - }, [reportActionIDFromRoute, allReportActions]); + }, [reportActionIDFromRoute, sortedAllReportActions]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. useLayoutEffect(() => { @@ -510,14 +504,14 @@ function ReportScreen({ }; const isLinkedReportActionDeleted = useMemo(() => { - if (!reportActionIDFromRoute || !allReportActions) { + if (!reportActionIDFromRoute || !sortedAllReportActions) { return false; } - const action = allReportActions[reportActionIDFromRoute]; + const action = sortedAllReportActions.find(item => item.reportActionID === reportActionIDFromRoute); return action && ReportActionsUtils.isDeletedAction(action); - }, [reportActionIDFromRoute, allReportActions]); + }, [reportActionIDFromRoute, sortedAllReportActions]); - if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { + if (isLinkedReportActionDeleted ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { return ( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, + selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, @@ -680,7 +675,7 @@ export default withViewportOffsetTop( ReportScreen, (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - lodashIsEqual(prevProps.allReportActions, nextProps.allReportActions) && + lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && prevProps.isComposerFullSize === nextProps.isComposerFullSize && lodashIsEqual(prevProps.betas, nextProps.betas) && From 2c809c42faa17e3e2e4a214f761822beb23e0911 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 13 Mar 2024 09:21:15 +0100 Subject: [PATCH 118/124] lint --- src/pages/home/ReportScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 6bd07f044dba..7561f02e9aaa 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -507,7 +507,7 @@ function ReportScreen({ if (!reportActionIDFromRoute || !sortedAllReportActions) { return false; } - const action = sortedAllReportActions.find(item => item.reportActionID === reportActionIDFromRoute); + const action = sortedAllReportActions.find((item) => item.reportActionID === reportActionIDFromRoute); return action && ReportActionsUtils.isDeletedAction(action); }, [reportActionIDFromRoute, sortedAllReportActions]); From c108d9d6e17912bc86d1179e7030419c3d27a7cf Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Mar 2024 11:15:50 +0100 Subject: [PATCH 119/124] add comments, move usePaginatedReportActionList to the utils --- src/libs/ReportActionsUtils.ts | 103 +++++++++++++++++- src/pages/home/ReportScreen.tsx | 2 +- src/pages/home/report/ReportActionsView.tsx | 98 +---------------- .../index.ts | 4 +- 4 files changed, 106 insertions(+), 101 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f67878a71fe0..def420bba7ff 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,11 +1,16 @@ +import type {RouteProp} from '@react-navigation/native'; import fastMerge from 'expensify-common/lib/fastMerge'; import _ from 'lodash'; import lodashFindLast from 'lodash/findLast'; +import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import getInitialPaginationSize from '@pages/home/report/getInitialPaginationSize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; import type { ActionName, ChangeLog, @@ -26,6 +31,7 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; +import type {CentralPaneNavigatorParamList} from './Navigation/types'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import type {OptimisticIOUReportAction} from './ReportUtils'; @@ -575,9 +581,7 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null | } if (shouldIncludeInvisibleActions) { - filteredReportActions = Object.values(reportActions).filter( - (action): action is ReportAction => !(action?.originalMessage as OriginalMessageActionableMentionWhisper['originalMessage'])?.resolution, - ); + filteredReportActions = Object.values(reportActions); } else { filteredReportActions = Object.entries(reportActions) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) @@ -976,6 +980,98 @@ function getReportActionMessageText(reportAction: OnyxEntry | Empt return reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, '') ?? ''; } +let listIDCount = Math.round(Math.random() * 100); +/** + * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, + * and manages pagination through 'handleReportActionPagination' function. + * + * linkedID - ID of the linked message used for initial focus. + * allReportActions - Array of messages. + * fetchNewerReportActions - Function to fetch more messages. + * route - Current route, used to reset states on route change. + * isLoading - Loading state indicator. + * triggerListID - Used to trigger a listID change. + * returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +const usePaginatedReportActionList = ( + linkedID: string, + localAllReportActions: OnyxTypes.ReportAction[], + fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, + route: RouteProp, + isLoading: boolean, + triggerListID: boolean, +) => { + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); + + useLayoutEffect(() => { + setCurrentReportActionID(''); + }, [route]); + + const listID = useMemo(() => { + isFirstLinkedActionRender.current = true; + listIDCount += 1; + return listIDCount; + // This needs to be triggered with each navigation to a comment. It happens when the route is changed. triggerListID is needed to trigger a listID change after initial mounting. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, triggerListID]); + + const index = useMemo(() => { + if (!linkedID || isLoading) { + return -1; + } + + return localAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); + }, [localAllReportActions, currentReportActionID, linkedID, isLoading]); + + const visibleReportActions = useMemo(() => { + if (!linkedID) { + return localAllReportActions; + } + if (isLoading || index === -1) { + return []; + } + + if (isFirstLinkedActionRender.current) { + return localAllReportActions.slice(index, localAllReportActions.length); + } + const paginationSize = getInitialPaginationSize(); + const newStartIndex = index >= paginationSize ? index - paginationSize : 0; + return newStartIndex ? localAllReportActions.slice(newStartIndex, localAllReportActions.length) : localAllReportActions; + // currentReportActionID is needed to trigger batching once the report action has been positioned + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [linkedID, localAllReportActions, index, isLoading, currentReportActionID]); + + const hasMoreCached = visibleReportActions.length < localAllReportActions?.length; + const newestReportAction = visibleReportActions?.[0]; + + const handleReportActionPagination = useCallback( + ({firstReportActionID}: {firstReportActionID: string}) => { + // This function is a placeholder as the actual pagination is handled by visibleReportActions + if (!hasMoreCached) { + isFirstLinkedActionRender.current = false; + fetchNewerReportActions(newestReportAction); + } + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; + } + setCurrentReportActionID(firstReportActionID); + }, + [fetchNewerReportActions, hasMoreCached, newestReportAction], + ); + + return { + visibleReportActions, + loadMoreReportActionsHandler: handleReportActionPagination, + linkedIdIndex: index, + listID, + }; +}; + export { extractLinksFromMessageHtml, getAllReportActions, @@ -1033,6 +1129,7 @@ export { isCurrentActionUnread, isActionableJoinRequest, isActionableJoinRequestPending, + usePaginatedReportActionList, }; export type {LastVisibleMessage}; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index d046bce95d2b..a216aaeb2dc1 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -334,7 +334,7 @@ function ReportScreen({ return; } - // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that + // It is possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. if (report.reportID && report.reportID === reportIDFromRoute && !reportMetadata?.isLoadingInitialReportActions) { diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 403855fede39..6fd5c5ab52b8 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,7 +1,7 @@ import type {RouteProp} from '@react-navigation/native'; import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; -import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -25,7 +25,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import getInitialPaginationSize from './getInitialPaginationSize'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import ReportActionsList from './ReportActionsList'; @@ -60,98 +59,6 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; -let listIDCount = Math.round(Math.random() * 100); - -/** - * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. - * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, - * and manages pagination through 'handleReportActionPagination' function. - * - * linkedID - ID of the linked message used for initial focus. - * allReportActions - Array of messages. - * fetchNewerReportActions - Function to fetch more messages. - * route - Current route, used to reset states on route change. - * isLoading - Loading state indicator. - * triggerListID - Used to trigger a listID change. - * returns {object} An object containing the sliced message array, the pagination function, - * index of the linked message, and a unique list ID. - */ -const usePaginatedReportActionList = ( - linkedID: string, - allReportActions: OnyxTypes.ReportAction[], - fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: RouteProp, - isLoading: boolean, - triggerListID: boolean, -) => { - // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. - // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned - const [currentReportActionID, setCurrentReportActionID] = useState(''); - const isFirstLinkedActionRender = useRef(true); - - useLayoutEffect(() => { - setCurrentReportActionID(''); - }, [route]); - - const listID = useMemo(() => { - isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, triggerListID]); - - const index = useMemo(() => { - if (!linkedID || isLoading) { - return -1; - } - - return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); - }, [allReportActions, currentReportActionID, linkedID, isLoading]); - - const visibleReportActions = useMemo(() => { - if (!linkedID) { - return allReportActions; - } - if (isLoading || index === -1) { - return []; - } - - if (isFirstLinkedActionRender.current) { - return allReportActions.slice(index, allReportActions.length); - } - const paginationSize = getInitialPaginationSize(); - const newStartIndex = index >= paginationSize ? index - paginationSize : 0; - return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; - // currentReportActionID is needed to trigger batching once the report action has been positioned - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); - - const hasMoreCached = visibleReportActions.length < allReportActions.length; - const newestReportAction = visibleReportActions?.[0]; - - const handleReportActionPagination = useCallback( - ({firstReportActionID}: {firstReportActionID: string}) => { - // This function is a placeholder as the actual pagination is handled by visibleReportActions - if (!hasMoreCached) { - isFirstLinkedActionRender.current = false; - fetchNewerReportActions(newestReportAction); - } - if (isFirstLinkedActionRender.current) { - isFirstLinkedActionRender.current = false; - } - setCurrentReportActionID(firstReportActionID); - }, - [fetchNewerReportActions, hasMoreCached, newestReportAction], - ); - - return { - visibleReportActions, - loadMoreReportActionsHandler: handleReportActionPagination, - linkedIdIndex: index, - listID, - }; -}; - function ReportActionsView({ report, session, @@ -202,7 +109,7 @@ function ReportActionsView({ loadMoreReportActionsHandler, linkedIdIndex, listID, - } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); + } = ReportActionsUtils.usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActions = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; @@ -453,7 +360,6 @@ function ReportActionsView({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} - // isLinkingLoader={!!reportActionID && isLoadingInitialReportActions} isLoadingInitialReportActions={isLoadingInitialReportActions} isLoadingOlderReportActions={isLoadingOlderReportActions} isLoadingNewerReportActions={isLoadingNewerReportActions} diff --git a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts index 68ff8c4cab3f..cb1f0dfdcded 100644 --- a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts +++ b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts @@ -1,5 +1,7 @@ +const DEFAULT_NUM_TO_RENDER = 50; + function getInitialNumToRender(numToRender: number): number { // For web and desktop environments, it's crucial to set this value equal to or higher than the maxToRenderPerBatch setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. - return Math.max(numToRender, 50); + return Math.max(numToRender, DEFAULT_NUM_TO_RENDER); } export default getInitialNumToRender; From 1e0386d8566820c97c04a44febd1ec892aa8e839 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 09:50:00 +0100 Subject: [PATCH 120/124] move usePaginatedReportActionList back --- src/libs/ReportActionsUtils.ts | 93 ------------------ src/pages/home/report/ReportActionsView.tsx | 97 ++++++++++++++++++- .../getInitialPaginationSize/index.native.ts | 5 +- .../report/getInitialPaginationSize/index.ts | 5 +- 4 files changed, 97 insertions(+), 103 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8b1d5dc94f85..8678d10d3f89 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1021,98 +1021,6 @@ function getReportActionMessageText(reportAction: OnyxEntry | Empt return reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, '') ?? ''; } -let listIDCount = Math.round(Math.random() * 100); -/** - * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. - * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, - * and manages pagination through 'handleReportActionPagination' function. - * - * linkedID - ID of the linked message used for initial focus. - * allReportActions - Array of messages. - * fetchNewerReportActions - Function to fetch more messages. - * route - Current route, used to reset states on route change. - * isLoading - Loading state indicator. - * triggerListID - Used to trigger a listID change. - * returns {object} An object containing the sliced message array, the pagination function, - * index of the linked message, and a unique list ID. - */ -const usePaginatedReportActionList = ( - linkedID: string, - localAllReportActions: OnyxTypes.ReportAction[], - fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: RouteProp, - isLoading: boolean, - triggerListID: boolean, -) => { - // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. - // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned - const [currentReportActionID, setCurrentReportActionID] = useState(''); - const isFirstLinkedActionRender = useRef(true); - - useLayoutEffect(() => { - setCurrentReportActionID(''); - }, [route]); - - const listID = useMemo(() => { - isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; - // This needs to be triggered with each navigation to a comment. It happens when the route is changed. triggerListID is needed to trigger a listID change after initial mounting. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, triggerListID]); - - const index = useMemo(() => { - if (!linkedID || isLoading) { - return -1; - } - - return localAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); - }, [localAllReportActions, currentReportActionID, linkedID, isLoading]); - - const visibleReportActions = useMemo(() => { - if (!linkedID) { - return localAllReportActions; - } - if (isLoading || index === -1) { - return []; - } - - if (isFirstLinkedActionRender.current) { - return localAllReportActions.slice(index, localAllReportActions.length); - } - const paginationSize = getInitialPaginationSize(); - const newStartIndex = index >= paginationSize ? index - paginationSize : 0; - return newStartIndex ? localAllReportActions.slice(newStartIndex, localAllReportActions.length) : localAllReportActions; - // currentReportActionID is needed to trigger batching once the report action has been positioned - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, localAllReportActions, index, isLoading, currentReportActionID]); - - const hasMoreCached = visibleReportActions.length < localAllReportActions?.length; - const newestReportAction = visibleReportActions?.[0]; - - const handleReportActionPagination = useCallback( - ({firstReportActionID}: {firstReportActionID: string}) => { - // This function is a placeholder as the actual pagination is handled by visibleReportActions - if (!hasMoreCached) { - isFirstLinkedActionRender.current = false; - fetchNewerReportActions(newestReportAction); - } - if (isFirstLinkedActionRender.current) { - isFirstLinkedActionRender.current = false; - } - setCurrentReportActionID(firstReportActionID); - }, - [fetchNewerReportActions, hasMoreCached, newestReportAction], - ); - - return { - visibleReportActions, - loadMoreReportActionsHandler: handleReportActionPagination, - linkedIdIndex: index, - listID, - }; -}; - export { extractLinksFromMessageHtml, getAllReportActions, @@ -1172,7 +1080,6 @@ export { isCurrentActionUnread, isActionableJoinRequest, isActionableJoinRequestPending, - usePaginatedReportActionList, }; export type {LastVisibleMessage}; diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 9b40f7ca9c3a..bd5415478f04 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,7 +1,7 @@ import type {RouteProp} from '@react-navigation/native'; import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; -import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -27,6 +27,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import ReportActionsList from './ReportActionsList'; +import getInitialPaginationSize from './getInitialPaginationSize'; type ReportActionsViewOnyxProps = { /** Session info for the currently logged in user. */ @@ -59,6 +60,98 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; +let listIDCount = Math.round(Math.random() * 100); + +/** + * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, + * and manages pagination through 'handleReportActionPagination' function. + * + * linkedID - ID of the linked message used for initial focus. + * allReportActions - Array of messages. + * fetchNewerReportActions - Function to fetch more messages. + * route - Current route, used to reset states on route change. + * isLoading - Loading state indicator. + * triggerListID - Used to trigger a listID change. + * returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +function usePaginatedReportActionList ( + linkedID: string, + allReportActions: OnyxTypes.ReportAction[], + fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, + route: RouteProp, + isLoading: boolean, + triggerListID: boolean, +) { + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); + + useLayoutEffect(() => { + setCurrentReportActionID(''); + }, [route]); + + const listID = useMemo(() => { + isFirstLinkedActionRender.current = true; + listIDCount += 1; + return listIDCount; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, triggerListID]); + + const index = useMemo(() => { + if (!linkedID || isLoading) { + return -1; + } + + return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); + }, [allReportActions, currentReportActionID, linkedID, isLoading]); + + const visibleReportActions = useMemo(() => { + if (!linkedID) { + return allReportActions; + } + if (isLoading || index === -1) { + return []; + } + + if (isFirstLinkedActionRender.current) { + return allReportActions.slice(index, allReportActions.length); + } + const paginationSize = getInitialPaginationSize; + const newStartIndex = index >= paginationSize ? index - paginationSize : 0; + return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; + // currentReportActionID is needed to trigger batching once the report action has been positioned + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); + + const hasMoreCached = visibleReportActions.length < allReportActions.length; + const newestReportAction = visibleReportActions?.[0]; + + const handleReportActionPagination = useCallback( + ({firstReportActionID}: {firstReportActionID: string}) => { + // This function is a placeholder as the actual pagination is handled by visibleReportActions + if (!hasMoreCached) { + isFirstLinkedActionRender.current = false; + fetchNewerReportActions(newestReportAction); + } + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; + } + setCurrentReportActionID(firstReportActionID); + }, + [fetchNewerReportActions, hasMoreCached, newestReportAction], + ); + + return { + visibleReportActions, + loadMoreReportActionsHandler: handleReportActionPagination, + linkedIdIndex: index, + listID, + }; +}; + function ReportActionsView({ report, session, @@ -109,7 +202,7 @@ function ReportActionsView({ loadMoreReportActionsHandler, linkedIdIndex, listID, - } = ReportActionsUtils.usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); + } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActions = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; diff --git a/src/pages/home/report/getInitialPaginationSize/index.native.ts b/src/pages/home/report/getInitialPaginationSize/index.native.ts index 69dbf5025ac5..195448f7e450 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.native.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.native.ts @@ -1,6 +1,3 @@ import CONST from '@src/CONST'; -function getInitialPaginationSize(): number { - return CONST.MOBILE_PAGINATION_SIZE; -} -export default getInitialPaginationSize; +export default CONST.MOBILE_PAGINATION_SIZE; diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/pages/home/report/getInitialPaginationSize/index.ts index d1467c0325b7..cc53d086b7b8 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.ts @@ -1,6 +1,3 @@ import CONST from '@src/CONST'; -function getInitialPaginationSize(): number { - return CONST.WEB_PAGINATION_SIZE; -} -export default getInitialPaginationSize; +export default CONST.WEB_PAGINATION_SIZE From 7b9e7e33c80b057efd549e3d38c6a50673688376 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 12:06:19 +0100 Subject: [PATCH 121/124] renaming --- src/pages/home/report/ReportActionsView.tsx | 106 +++++++++----------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index bd5415478f04..1452637800a4 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -25,9 +25,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import getInitialPaginationSize from './getInitialPaginationSize'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import ReportActionsList from './ReportActionsList'; -import getInitialPaginationSize from './getInitialPaginationSize'; type ReportActionsViewOnyxProps = { /** Session info for the currently logged in user. */ @@ -76,7 +76,7 @@ let listIDCount = Math.round(Math.random() * 100); * returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -function usePaginatedReportActionList ( +function usePaginatedReportActionList( linkedID: string, allReportActions: OnyxTypes.ReportAction[], fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, @@ -117,11 +117,10 @@ function usePaginatedReportActionList ( } if (isFirstLinkedActionRender.current) { - return allReportActions.slice(index, allReportActions.length); + return allReportActions.slice(index); } const paginationSize = getInitialPaginationSize; - const newStartIndex = index >= paginationSize ? index - paginationSize : 0; - return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; + return allReportActions.slice(Math.max(index - paginationSize, 0)); // currentReportActionID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); @@ -147,10 +146,10 @@ function usePaginatedReportActionList ( return { visibleReportActions, loadMoreReportActionsHandler: handleReportActionPagination, - linkedIdIndex: index, + linkedIDIndex: index, listID, }; -}; +} function ReportActionsView({ report, @@ -170,14 +169,13 @@ function ReportActionsView({ const didSubscribeToReportTypingEvents = useRef(false); const network = useNetwork(); - const {isSmallScreenWidth} = useWindowDimensions(); + const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); - const {windowHeight} = useWindowDimensions(); const isFocused = useIsFocused(); const prevNetworkRef = useRef(network); const prevAuthTokenType = usePrevious(session?.authTokenType); - const [isInitialLinkedView, setIsInitialLinkedView] = useState(!!reportActionID); + const [isNavigatingToLinkedMessage, setNavigatingToLinkedMessage] = useState(!!reportActionID); const prevIsSmallScreenWidthRef = useRef(isSmallScreenWidth); const reportID = report.reportID; const isLoading = (!!reportActionID && isLoadingInitialReportActions) || !isReadyForCommentLinking; @@ -200,16 +198,15 @@ function ReportActionsView({ const { visibleReportActions: reportActions, loadMoreReportActionsHandler, - linkedIdIndex, + linkedIDIndex, listID, } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); - const hasCachedActions = useInitialValue(() => reportActions.length > 0); + const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; - const newestReportAction = reportActions?.[0]; + const newestReportAction = useMemo(() => reportActions?.[0], [reportActions]); const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; - const firstReportActionName = reportActions?.[0]?.actionName; const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); @@ -338,39 +335,32 @@ function ReportActionsView({ Report.getOlderActions(reportID, oldestReportAction.reportActionID); }, [network.isOffline, isLoadingOlderReportActions, isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID]); - // const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); - const firstReportActionID = useMemo(() => newestReportAction?.reportActionID, [newestReportAction]); - const loadNewerChats = useCallback( - // eslint-disable-next-line rulesdir/prefer-early-return - () => { - if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - return; - } - // Determines if loading older reports is necessary when the content is smaller than the list - // and there are fewer than 23 items, indicating we've reached the oldest message. - const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; - - if ( - (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || - (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) - ) { - loadMoreReportActionsHandler({firstReportActionID}); - } - }, - [ - isLoadingInitialReportActions, - isLoadingOlderReportActions, - checkIfContentSmallerThanList, - reportActionID, - linkedIdIndex, - hasNewestReportAction, - loadMoreReportActionsHandler, - firstReportActionID, - network.isOffline, - reportActions.length, - newestReportAction, - ], - ); + const loadNewerChats = useCallback(() => { + if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return; + } + // Determines if loading older reports is necessary when the content is smaller than the list + // and there are fewer than 23 items, indicating we've reached the oldest message. + const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; + + if ( + (reportActionID && linkedIDIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || + (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) + ) { + loadMoreReportActionsHandler({firstReportActionID: newestReportAction?.reportActionID}); + } + }, [ + isLoadingInitialReportActions, + isLoadingOlderReportActions, + checkIfContentSmallerThanList, + reportActionID, + linkedIDIndex, + hasNewestReportAction, + loadMoreReportActionsHandler, + network.isOffline, + reportActions.length, + newestReportAction, + ]); /** * Runs when the FlatList finishes laying out @@ -384,7 +374,7 @@ function ReportActionsView({ } didLayout.current = true; - Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); // Capture the init measurement only once not per each chat switch as the value gets overwritten if (!ReportActionsView.initMeasured) { @@ -394,7 +384,7 @@ function ReportActionsView({ Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } }, - [hasCachedActions], + [hasCachedActionOnFirstRender], ); useEffect(() => { @@ -402,7 +392,7 @@ function ReportActionsView({ // This code should be removed once REPORTPREVIEW is no longer repositioned. // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. const shouldOpenReport = - firstReportActionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && + newestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && !hasCreatedAction && isReadyForCommentLinking && reportActions.length < 24 && @@ -419,7 +409,7 @@ function ReportActionsView({ reportID, reportActions, reportActionID, - firstReportActionName, + newestReportAction?.actionName, isReadyForCommentLinking, isLoadingOlderReportActions, isLoadingNewerReportActions, @@ -427,27 +417,27 @@ function ReportActionsView({ ]); // Check if the first report action in the list is the one we're currently linked to - const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; + const isTheFirstReportActionIsLinked = newestReportAction?.reportActionID === reportActionID; useEffect(() => { - let timerId: NodeJS.Timeout; + let timerID: NodeJS.Timeout; if (isTheFirstReportActionIsLinked) { - setIsInitialLinkedView(true); + setNavigatingToLinkedMessage(true); } else { // After navigating to the linked reportAction, apply this to correctly set // `autoscrollToTopThreshold` prop when linking to a specific reportAction. InteractionManager.runAfterInteractions(() => { // Using a short delay to ensure the view is updated after interactions - timerId = setTimeout(() => setIsInitialLinkedView(false), 10); + timerID = setTimeout(() => setNavigatingToLinkedMessage(false), 10); }); } return () => { - if (!timerId) { + if (!timerID) { return; } - clearTimeout(timerId); + clearTimeout(timerID); }; }, [isTheFirstReportActionIsLinked]); @@ -456,7 +446,7 @@ function ReportActionsView({ return null; } // AutoScroll is disabled when we do linking to a specific reportAction - const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isInitialLinkedView); + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isNavigatingToLinkedMessage); return ( <> From b4d1e398d5af0c6c1839efb4090dbd1d9d105a2e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 13:58:12 +0100 Subject: [PATCH 122/124] remove unused layoutListHeight --- src/pages/home/report/ReportActionsView.tsx | 34 +++++++++------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 1452637800a4..b5df5e78d55a 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -171,7 +171,6 @@ function ReportActionsView({ const network = useNetwork(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const contentListHeight = useRef(0); - const layoutListHeight = useRef(0); const isFocused = useIsFocused(); const prevNetworkRef = useRef(network); const prevAuthTokenType = usePrevious(session?.authTokenType); @@ -365,27 +364,22 @@ function ReportActionsView({ /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = useCallback( - (e: LayoutChangeEvent) => { - layoutListHeight.current = e.nativeEvent.layout.height; - - if (didLayout.current) { - return; - } + const recordTimeToMeasureItemLayout = useCallback(() => { + if (didLayout.current) { + return; + } - didLayout.current = true; - Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); + didLayout.current = true; + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); - // Capture the init measurement only once not per each chat switch as the value gets overwritten - if (!ReportActionsView.initMeasured) { - Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); - ReportActionsView.initMeasured = true; - } else { - Performance.markEnd(CONST.TIMING.SWITCH_REPORT); - } - }, - [hasCachedActionOnFirstRender], - ); + // Capture the init measurement only once not per each chat switch as the value gets overwritten + if (!ReportActionsView.initMeasured) { + Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); + ReportActionsView.initMeasured = true; + } else { + Performance.markEnd(CONST.TIMING.SWITCH_REPORT); + } + }, [hasCachedActionOnFirstRender]); useEffect(() => { // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP From acb2b06a5dc2fabf4c53f9c101baeecb1d12fc79 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 20:16:40 +0100 Subject: [PATCH 123/124] move content of usePaginatedReportActionList to ReportActionsView --- src/pages/home/report/ReportActionsView.tsx | 176 +++++++----------- .../report/getInitialPaginationSize/index.ts | 2 +- 2 files changed, 70 insertions(+), 108 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index b5df5e78d55a..970c8c970f8f 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -3,7 +3,6 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; -import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; @@ -62,95 +61,6 @@ const SPACER = 16; let listIDCount = Math.round(Math.random() * 100); -/** - * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. - * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, - * and manages pagination through 'handleReportActionPagination' function. - * - * linkedID - ID of the linked message used for initial focus. - * allReportActions - Array of messages. - * fetchNewerReportActions - Function to fetch more messages. - * route - Current route, used to reset states on route change. - * isLoading - Loading state indicator. - * triggerListID - Used to trigger a listID change. - * returns {object} An object containing the sliced message array, the pagination function, - * index of the linked message, and a unique list ID. - */ -function usePaginatedReportActionList( - linkedID: string, - allReportActions: OnyxTypes.ReportAction[], - fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: RouteProp, - isLoading: boolean, - triggerListID: boolean, -) { - // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. - // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned - const [currentReportActionID, setCurrentReportActionID] = useState(''); - const isFirstLinkedActionRender = useRef(true); - - useLayoutEffect(() => { - setCurrentReportActionID(''); - }, [route]); - - const listID = useMemo(() => { - isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, triggerListID]); - - const index = useMemo(() => { - if (!linkedID || isLoading) { - return -1; - } - - return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); - }, [allReportActions, currentReportActionID, linkedID, isLoading]); - - const visibleReportActions = useMemo(() => { - if (!linkedID) { - return allReportActions; - } - if (isLoading || index === -1) { - return []; - } - - if (isFirstLinkedActionRender.current) { - return allReportActions.slice(index); - } - const paginationSize = getInitialPaginationSize; - return allReportActions.slice(Math.max(index - paginationSize, 0)); - // currentReportActionID is needed to trigger batching once the report action has been positioned - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); - - const hasMoreCached = visibleReportActions.length < allReportActions.length; - const newestReportAction = visibleReportActions?.[0]; - - const handleReportActionPagination = useCallback( - ({firstReportActionID}: {firstReportActionID: string}) => { - // This function is a placeholder as the actual pagination is handled by visibleReportActions - if (!hasMoreCached) { - isFirstLinkedActionRender.current = false; - fetchNewerReportActions(newestReportAction); - } - if (isFirstLinkedActionRender.current) { - isFirstLinkedActionRender.current = false; - } - setCurrentReportActionID(firstReportActionID); - }, - [fetchNewerReportActions, hasMoreCached, newestReportAction], - ); - - return { - visibleReportActions, - loadMoreReportActionsHandler: handleReportActionPagination, - linkedIDIndex: index, - listID, - }; -} - function ReportActionsView({ report, session, @@ -168,6 +78,11 @@ function ReportActionsView({ const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest,we don't maintain their position and instead trigger a recalculation of their positioning in the list. + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); + const network = useNetwork(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const contentListHeight = useRef(0); @@ -194,19 +109,6 @@ function ReportActionsView({ [isLoadingNewerReportActions, isLoadingInitialReportActions, reportID], ); - const { - visibleReportActions: reportActions, - loadMoreReportActionsHandler, - linkedIDIndex, - listID, - } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); - const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); - const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); - const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; - const newestReportAction = useMemo(() => reportActions?.[0], [reportActions]); - const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); - const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; - const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); const openReportIfNecessary = () => { @@ -219,6 +121,66 @@ function ReportActionsView({ Report.openReport(reportID, reportActionID); }; + useLayoutEffect(() => { + setCurrentReportActionID(''); + }, [route]); + + const listID = useMemo(() => { + isFirstLinkedActionRender.current = true; + listIDCount += 1; + return listIDCount; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, isLoadingInitialReportActions]); + + const indexOfLinkedAction = useMemo(() => { + if (!reportActionID || isLoading) { + return -1; + } + + return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? reportActionID : currentReportActionID)); + }, [allReportActions, currentReportActionID, reportActionID, isLoading]); + + const reportActions = useMemo(() => { + if (!reportActionID) { + return allReportActions; + } + if (isLoading || indexOfLinkedAction === -1) { + return []; + } + + if (isFirstLinkedActionRender.current) { + return allReportActions.slice(indexOfLinkedAction); + } + const paginationSize = getInitialPaginationSize; + return allReportActions.slice(Math.max(indexOfLinkedAction - paginationSize, 0)); + // currentReportActionID is needed to trigger batching once the report action has been positioned + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportActionID, allReportActions, indexOfLinkedAction, isLoading, currentReportActionID]); + + const hasMoreCached = reportActions.length < allReportActions.length; + const newestReportAction = useMemo(() => reportActions?.[0], [reportActions]); + const handleReportActionPagination = useCallback( + ({firstReportActionID}: {firstReportActionID: string}) => { + // This function is a placeholder as the actual pagination is handled by visibleReportActions + if (!hasMoreCached) { + isFirstLinkedActionRender.current = false; + fetchNewerAction(newestReportAction); + } + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; + } + setCurrentReportActionID(firstReportActionID); + }, + [fetchNewerAction, hasMoreCached, newestReportAction], + ); + + const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); + const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); + const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; + + const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); + const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; + useEffect(() => { if (reportActionID) { return; @@ -343,19 +305,19 @@ function ReportActionsView({ const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; if ( - (reportActionID && linkedIDIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || + (reportActionID && indexOfLinkedAction > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) ) { - loadMoreReportActionsHandler({firstReportActionID: newestReportAction?.reportActionID}); + handleReportActionPagination({firstReportActionID: newestReportAction?.reportActionID}); } }, [ isLoadingInitialReportActions, isLoadingOlderReportActions, checkIfContentSmallerThanList, reportActionID, - linkedIDIndex, + indexOfLinkedAction, hasNewestReportAction, - loadMoreReportActionsHandler, + handleReportActionPagination, network.isOffline, reportActions.length, newestReportAction, diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/pages/home/report/getInitialPaginationSize/index.ts index cc53d086b7b8..87ec6856aa20 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.ts @@ -1,3 +1,3 @@ import CONST from '@src/CONST'; -export default CONST.WEB_PAGINATION_SIZE +export default CONST.WEB_PAGINATION_SIZE; From cb86c85f08a5d56a027860c5364bae68533253cf Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 21:17:28 +0100 Subject: [PATCH 124/124] update listID --- src/libs/NumberUtils.ts | 10 +++++++++- src/libs/ReportActionsUtils.ts | 6 ------ src/pages/home/report/ReportActionsView.tsx | 8 +++++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/libs/NumberUtils.ts b/src/libs/NumberUtils.ts index 62d6fa00906a..2dfc1e722c58 100644 --- a/src/libs/NumberUtils.ts +++ b/src/libs/NumberUtils.ts @@ -92,4 +92,12 @@ function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } -export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale, roundDownToLargestMultiple, roundToTwoDecimalPlaces, clamp}; +function generateNewRandomInt(old: number, min: number, max: number): number { + let newNum = old; + while (newNum === old) { + newNum = generateRandomInt(min, max); + } + return newNum; +} + +export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale, roundDownToLargestMultiple, roundToTwoDecimalPlaces, clamp, generateNewRandomInt}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8678d10d3f89..f069bcfa58ae 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,16 +1,11 @@ -import type {RouteProp} from '@react-navigation/native'; import fastMerge from 'expensify-common/lib/fastMerge'; import _ from 'lodash'; import lodashFindLast from 'lodash/findLast'; -import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import getInitialPaginationSize from '@pages/home/report/getInitialPaginationSize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; import type { ActionName, ChangeLog, @@ -31,7 +26,6 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; -import type {CentralPaneNavigatorParamList} from './Navigation/types'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import type {OptimisticIOUReportAction} from './ReportUtils'; diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 970c8c970f8f..520a9a3604c5 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -12,6 +12,7 @@ import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import {generateNewRandomInt} from '@libs/NumberUtils'; import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; @@ -59,7 +60,7 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; -let listIDCount = Math.round(Math.random() * 100); +let listOldID = Math.round(Math.random() * 100); function ReportActionsView({ report, @@ -127,8 +128,9 @@ function ReportActionsView({ const listID = useMemo(() => { isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; + const newID = generateNewRandomInt(listOldID, 1, Number.MAX_SAFE_INTEGER); + listOldID = newID; + return newID; // eslint-disable-next-line react-hooks/exhaustive-deps }, [route, isLoadingInitialReportActions]);