From 5258cff5cf967d8fa06b3b29d29195a63d64b0f6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:50:57 +0200 Subject: [PATCH 0001/1113] 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 0002/1113] 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 0003/1113] 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 0004/1113] 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 0005/1113] 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 0006/1113] 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 0007/1113] 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 0008/1113] 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 0009/1113] 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 0010/1113] 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 0011/1113] 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 0012/1113] 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 c8692c0887191667c1335814eb05f2a77c90ee2a Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 27 Oct 2023 00:44:15 +0700 Subject: [PATCH 0013/1113] fix: app uses ultra-wide camera by default --- src/pages/iou/ReceiptSelector/index.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index ca9fe90575e7..9181d0bd2701 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -1,5 +1,5 @@ import {View, Text, PixelRatio, ActivityIndicator, PanResponder} from 'react-native'; -import React, {useCallback, useContext, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import lodashGet from 'lodash/get'; import _ from 'underscore'; import PropTypes from 'prop-types'; @@ -84,6 +84,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); + const normalCameraDeviceIdRef = useRef(null); const hideReciptModal = () => { setIsAttachmentInvalid(false); @@ -169,6 +170,24 @@ function ReceiptSelector({route, transactionID, iou, report}) { }), ).current; + /** + * On phones that have ultra-wide lens, default camera is ultra-wide. + * The last deviceId is of regular len camera. + */ + useEffect(() => { + if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + normalCameraDeviceIdRef.current = _.chain(devices) + .filter((device) => device.kind === 'videoinput') + .last() + .get('deviceId', '') + .value(); + }); + }, []); + const mobileCameraView = () => ( <> @@ -197,7 +216,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}}} + videoConstraints={{facingMode: {exact: 'environment'}, deviceId: normalCameraDeviceIdRef.current}} torchOn={isFlashLightOn} onTorchAvailability={setIsTorchAvailable} forceScreenshotSourceSize From 3d3bd77d7c1d633ed42adef361952bc588db050f Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 27 Oct 2023 16:54:07 +0700 Subject: [PATCH 0014/1113] useState approach --- src/pages/iou/ReceiptSelector/index.js | 47 +++++++++++++++----------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 9181d0bd2701..601a1abd4773 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -84,7 +84,32 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); - const normalCameraDeviceIdRef = useRef(null); + const [videoConstraints, setVideoConstraints] = useState({facingMode: {exact: 'environment'}}); + + /** + * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. + * The last deviceId is of regular len camera. + */ + useEffect(() => { + navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(() => { + if (!navigator.mediaDevices.enumerateDevices) { + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .reverse() + .find((item) => item.label && item.label.endsWith('facing back')) + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + return; + } + setVideoConstraints({deviceId: lastBackDeviceId}); + }); + }); + }, []); const hideReciptModal = () => { setIsAttachmentInvalid(false); @@ -170,24 +195,6 @@ function ReceiptSelector({route, transactionID, iou, report}) { }), ).current; - /** - * On phones that have ultra-wide lens, default camera is ultra-wide. - * The last deviceId is of regular len camera. - */ - useEffect(() => { - if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - return; - } - - navigator.mediaDevices.enumerateDevices().then((devices) => { - normalCameraDeviceIdRef.current = _.chain(devices) - .filter((device) => device.kind === 'videoinput') - .last() - .get('deviceId', '') - .value(); - }); - }, []); - const mobileCameraView = () => ( <> @@ -216,7 +223,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}, deviceId: normalCameraDeviceIdRef.current}} + videoConstraints={videoConstraints} torchOn={isFlashLightOn} onTorchAvailability={setIsTorchAvailable} forceScreenshotSourceSize From 56f99e6edd04dbf09188dce97e9c61820be40f8c Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 30 Oct 2023 09:44:17 +0700 Subject: [PATCH 0015/1113] remove getUserMedia --- src/pages/iou/ReceiptSelector/index.js | 56 +++++++++++++------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 1ead9ca45076..168ee8230ff1 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -84,30 +84,30 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); - const [videoConstraints, setVideoConstraints] = useState({facingMode: {exact: 'environment'}}); + const [videoConstraints, setVideoConstraints] = useState(null); /** * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. * The last deviceId is of regular len camera. */ useEffect(() => { - navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(() => { - if (!navigator.mediaDevices.enumerateDevices) { + if (!navigator.mediaDevices.enumerateDevices) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .filter((item) => item.label && item.label.endsWith('facing back')) + .last() + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + setVideoConstraints({facingMode: {exact: 'environment'}}); return; } - - navigator.mediaDevices.enumerateDevices().then((devices) => { - const lastBackDeviceId = _.chain(devices) - .reverse() - .find((item) => item.label && item.label.endsWith('facing back')) - .get('deviceId', '') - .value(); - - if (!lastBackDeviceId) { - return; - } - setVideoConstraints({deviceId: lastBackDeviceId}); - }); + setVideoConstraints({deviceId: lastBackDeviceId}); }); }, []); @@ -217,17 +217,19 @@ function ReceiptSelector({route, transactionID, iou, report}) { {translate('receipt.cameraAccess')} )} - setCameraPermissionState('granted')} - onUserMediaError={() => setCameraPermissionState('denied')} - style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} - ref={cameraRef} - screenshotFormat="image/png" - videoConstraints={videoConstraints} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} - forceScreenshotSourceSize - /> + {videoConstraints && ( + setCameraPermissionState('granted')} + onUserMediaError={() => setCameraPermissionState('denied')} + style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} + ref={cameraRef} + screenshotFormat="image/png" + videoConstraints={videoConstraints} + torchOn={isFlashLightOn} + onTorchAvailability={setIsTorchAvailable} + forceScreenshotSourceSize + /> + )} From 3180d32f66ebd7cc30603e17cae250d3bb705368 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 30 Oct 2023 18:11:32 +0700 Subject: [PATCH 0016/1113] close active stream --- src/pages/iou/ReceiptSelector/index.js | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 168ee8230ff1..304d7c44da43 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -91,23 +91,27 @@ function ReceiptSelector({route, transactionID, iou, report}) { * The last deviceId is of regular len camera. */ useEffect(() => { - if (!navigator.mediaDevices.enumerateDevices) { - setVideoConstraints({facingMode: {exact: 'environment'}}); - return; - } + navigator.mediaDevices.getUserMedia({audio: true, video: true}).then((stream) => { + _.forEach(stream.getTracks(), (videoStream) => videoStream.stop()); - navigator.mediaDevices.enumerateDevices().then((devices) => { - const lastBackDeviceId = _.chain(devices) - .filter((item) => item.label && item.label.endsWith('facing back')) - .last() - .get('deviceId', '') - .value(); - - if (!lastBackDeviceId) { + if (!navigator.mediaDevices.enumerateDevices) { setVideoConstraints({facingMode: {exact: 'environment'}}); return; } - setVideoConstraints({deviceId: lastBackDeviceId}); + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .filter((item) => item.kind === 'videoinput') + .last() + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + setVideoConstraints({deviceId: lastBackDeviceId}); + }); }); }, []); From c853f697eafa8d3b070bf4bf518aae31f769e720 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 6 Nov 2023 12:22:43 +0700 Subject: [PATCH 0017/1113] do not request permission on desktop --- src/pages/iou/ReceiptSelector/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index fdef1a7af941..17323b3ae687 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -91,6 +91,10 @@ function ReceiptSelector({route, transactionID, iou, report}) { * The last deviceId is of regular len camera. */ useEffect(() => { + if (!Browser.isMobile()) { + return; + } + navigator.mediaDevices.getUserMedia({video: true}).then((stream) => { _.forEach(stream.getTracks(), (videoStream) => videoStream.stop()); From 1f073b03dc0192086ab3c215dd535a271dfb1e85 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 7 Nov 2023 19:59:23 +0100 Subject: [PATCH 0018/1113] 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 c7bbfd4cf1529e6ba40bf206e96bc70eee26c9a9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 8 Nov 2023 16:59:28 +0700 Subject: [PATCH 0019/1113] only request permission on Scan tab focus --- src/pages/iou/ReceiptSelector/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 4e482d13fb44..6d65d250f1a6 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -1,8 +1,10 @@ +import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, Text, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; import Shutter from '@assets/images/shutter.svg'; @@ -84,13 +86,14 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); const [videoConstraints, setVideoConstraints] = useState(null); + const isCameraActive = useIsFocused(); /** * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. * The last deviceId is of regular len camera. */ useEffect(() => { - if (!Browser.isMobile()) { + if (!_.isEmpty(videoConstraints) || !isCameraActive || !Browser.isMobile()) { return; } @@ -116,7 +119,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { setVideoConstraints({deviceId: lastBackDeviceId}); }); }); - }, []); + }, [isCameraActive]); const hideReciptModal = () => { setIsAttachmentInvalid(false); From 586259d96d984f6fdeb0dfab37700a8248e841f2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 8 Nov 2023 17:02:22 +0700 Subject: [PATCH 0020/1113] fix lint --- src/pages/iou/ReceiptSelector/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 6d65d250f1a6..63f382baa7ed 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -119,6 +119,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { setVideoConstraints({deviceId: lastBackDeviceId}); }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isCameraActive]); const hideReciptModal = () => { From 2b7a7355d25c7207cccd723b5322ef90884777b6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 12 Nov 2023 18:06:39 +0100 Subject: [PATCH 0021/1113] 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 0022/1113] 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 8b7f74c22a935f44879af65c652dec2c32360bbf Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 4 Dec 2023 15:58:16 +0700 Subject: [PATCH 0023/1113] bool check videoConstraints --- src/pages/iou/ReceiptSelector/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 67211f5c42ee..c68c20b5c643 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -229,7 +229,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { {translate('receipt.cameraAccess')} )} - {videoConstraints && ( + {!_.isEmpty(videoConstraints) && ( setCameraPermissionState('granted')} onUserMediaError={() => setCameraPermissionState('denied')} From 2d083d329e4f5e0b2677bff3dc55d7b0dd9497e5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 18 Dec 2023 13:16:23 +0100 Subject: [PATCH 0024/1113] 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 0025/1113] 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 0026/1113] 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 0027/1113] 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 0028/1113] 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 0029/1113] 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 0030/1113] 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 0031/1113] 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 0032/1113] 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 0033/1113] 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 0034/1113] 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 0035/1113] 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 88b7ef89bcaa07ff52a1f1dfd9505ebf2b338a0a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 21 Dec 2023 18:11:06 +0100 Subject: [PATCH 0036/1113] connect local react-fast-pdf --- package-lock.json | 429 +++++++++++++++++++++++++++++--- package.json | 1 + src/components/PDFView/index.js | 266 ++------------------ src/styles/index.ts | 19 -- 4 files changed, 426 insertions(+), 289 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c2ae325575d..cf30d9f6fb3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ "react-content-loader": "^6.1.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", + "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#ecb6779e8442d433ca421b7e05acedaa0725a30a", "react-map-gl": "^7.1.3", "react-native": "0.72.4", "react-native-android-location-enabler": "^1.2.2", @@ -5380,6 +5381,50 @@ "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", @@ -20854,6 +20899,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -20964,7 +21015,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "debug": "4" @@ -21416,7 +21467,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "devOptional": true }, "node_modules/are-docs-informative": { "version": "0.0.2", @@ -21431,7 +21482,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "delegates": "^1.0.0", @@ -21445,7 +21496,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -24072,6 +24123,21 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/canvas-size": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/canvas-size/-/canvas-size-1.2.6.tgz", @@ -24746,7 +24812,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "color-support": "bin.js" @@ -25081,7 +25147,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/constants-browserify": { @@ -26630,7 +26696,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/denodeify": { @@ -26706,6 +26772,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -31212,7 +31287,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -31729,7 +31804,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/has-value": { @@ -32467,7 +32542,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -37782,8 +37857,9 @@ } }, "node_modules/make-cancellable-promise": { - "version": "1.1.0", - "license": "MIT", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", "funding": { "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" } @@ -41068,7 +41144,6 @@ "version": "2.17.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "dev": true, "optional": true }, "node_modules/nanoid": { @@ -41330,6 +41405,21 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -41412,7 +41502,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "are-we-there-yet": "^2.0.0", @@ -42615,6 +42705,15 @@ "node": ">=8" } }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -44004,6 +44103,74 @@ "react": ">=16.13.1" } }, + "node_modules/react-fast-pdf": { + "version": "1.0.3", + "resolved": "git+ssh://git@github.com/rezkiy37/react-fast-pdf.git#ecb6779e8442d433ca421b7e05acedaa0725a30a", + "integrity": "sha512-kmdkx9Oh6gvMyt6Jiv+xSi5fQEybxXFD6l+woscs6Lt6m3nLweYP1KUSUn03+ocpVzLKIbLiMbW8D/D9R18AJA==", + "license": "MIT", + "dependencies": { + "react-pdf": "^7.3.3", + "react-window": "^1.8.9" + }, + "engines": { + "node": "20.9.0", + "npm": "10.1.0" + }, + "peerDependencies": { + "lodash": "4.x", + "prop-types": "15.x", + "react": "18.x", + "react-dom": "18.x" + } + }, + "node_modules/react-fast-pdf/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-fast-pdf/node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, + "node_modules/react-fast-pdf/node_modules/react-pdf": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.6.0.tgz", + "integrity": "sha512-b2/8V6xhe5pn4Y01ELKTQZ+RhdZl1KpSAMGbN+HCZ/kwhYTIc4Pn5ctz1wRQUu1gOJbIEG4CcjMD9vTCzNdwjw==", + "dependencies": { + "clsx": "^2.0.0", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.2.1", + "pdfjs-dist": "3.11.174", + "prop-types": "^15.6.2", + "tiny-invariant": "^1.0.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-freeze": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz", @@ -47571,6 +47738,61 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/simple-git": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.0.tgz", @@ -52409,7 +52631,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" @@ -56611,6 +56833,42 @@ "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true + } + } + } + } + }, "@mapbox/point-geometry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", @@ -67822,6 +68080,12 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -67896,7 +68160,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "devOptional": true, "requires": { "debug": "4" } @@ -68235,7 +68499,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "devOptional": true }, "are-docs-informative": { "version": "0.0.2", @@ -68247,7 +68511,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dev": true, + "devOptional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -68257,7 +68521,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -70191,6 +70455,17 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz", "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==" }, + "canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "optional": true, + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + } + }, "canvas-size": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/canvas-size/-/canvas-size-1.2.6.tgz", @@ -70668,7 +70943,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true + "devOptional": true }, "colorette": { "version": "1.4.0", @@ -70910,7 +71185,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true + "devOptional": true }, "constants-browserify": { "version": "1.0.0", @@ -72010,7 +72285,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true + "devOptional": true }, "denodeify": { "version": "1.2.1", @@ -72065,6 +72340,12 @@ "repeat-string": "^1.5.4" } }, + "detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -75344,7 +75625,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "dev": true, + "devOptional": true, "requires": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -75694,7 +75975,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true + "devOptional": true }, "has-value": { "version": "1.0.0", @@ -76234,7 +76515,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, + "devOptional": true, "requires": { "agent-base": "6", "debug": "4" @@ -79943,7 +80224,9 @@ } }, "make-cancellable-promise": { - "version": "1.1.0" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==" }, "make-dir": { "version": "2.1.0", @@ -82337,7 +82620,6 @@ "version": "2.17.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "dev": true, "optional": true }, "nanoid": { @@ -82537,6 +82819,15 @@ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "requires": { + "abbrev": "1" + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -82594,7 +82885,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "dev": true, + "devOptional": true, "requires": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -83434,6 +83725,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true + }, "pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -84415,6 +84712,46 @@ "@babel/runtime": "^7.12.5" } }, + "react-fast-pdf": { + "version": "git+ssh://git@github.com/rezkiy37/react-fast-pdf.git#ecb6779e8442d433ca421b7e05acedaa0725a30a", + "integrity": "sha512-kmdkx9Oh6gvMyt6Jiv+xSi5fQEybxXFD6l+woscs6Lt6m3nLweYP1KUSUn03+ocpVzLKIbLiMbW8D/D9R18AJA==", + "from": "react-fast-pdf@git+https://github.com/rezkiy37/react-fast-pdf#ecb6779e8442d433ca421b7e05acedaa0725a30a", + "requires": { + "react-pdf": "^7.3.3", + "react-window": "^1.8.9" + }, + "dependencies": { + "clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" + }, + "pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "requires": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, + "react-pdf": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.6.0.tgz", + "integrity": "sha512-b2/8V6xhe5pn4Y01ELKTQZ+RhdZl1KpSAMGbN+HCZ/kwhYTIc4Pn5ctz1wRQUu1gOJbIEG4CcjMD9vTCzNdwjw==", + "requires": { + "clsx": "^2.0.0", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.2.1", + "pdfjs-dist": "3.11.174", + "prop-types": "^15.6.2", + "tiny-invariant": "^1.0.0", + "tiny-warning": "^1.0.0" + } + } + } + }, "react-freeze": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz", @@ -86952,6 +87289,40 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "optional": true + }, + "simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + }, + "dependencies": { + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true + } + } + }, "simple-git": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.0.tgz", @@ -90443,7 +90814,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, + "devOptional": true, "requires": { "string-width": "^1.0.2 || 2 || 3 || 4" } diff --git a/package.json b/package.json index 7281ab12cfa7..cde7a9460002 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "react-content-loader": "^6.1.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", + "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#ecb6779e8442d433ca421b7e05acedaa0725a30a", "react-map-gl": "^7.1.3", "react-native": "0.72.4", "react-native-android-location-enabler": "^1.2.2", diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index e18c52b06972..7f74b919469c 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -1,10 +1,8 @@ import 'core-js/features/array/at'; -import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker'; import React, {Component} from 'react'; +import {PDFPreviewer} from 'react-fast-pdf'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import {Document, Page, pdfjs} from 'react-pdf/dist/esm/entry.webpack'; -import {VariableSizeList as List} from 'react-window'; import _ from 'underscore'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -13,7 +11,6 @@ import withLocalize from '@components/withLocalize'; import withThemeStyles from '@components/withThemeStyles'; import withWindowDimensions from '@components/withWindowDimensions'; import compose from '@libs/compose'; -import Log from '@libs/Log'; import variables from '@styles/variables'; import * as CanvasSize from '@userActions/CanvasSize'; import CONST from '@src/CONST'; @@ -21,41 +18,15 @@ import ONYXKEYS from '@src/ONYXKEYS'; import PDFPasswordForm from './PDFPasswordForm'; import * as pdfViewPropTypes from './pdfViewPropTypes'; -/** - * Each page has a default border. The app should take this size into account - * when calculates the page width and height. - */ -const PAGE_BORDER = 9; -/** - * Pages should be more narrow than the container on large screens. The app should take this size into account - * when calculates the page width. - */ -const LARGE_SCREEN_SIDE_SPACING = 40; - class PDFView extends Component { constructor(props) { super(props); this.state = { - numPages: null, - pageViewports: [], - containerWidth: props.windowWidth, - containerHeight: props.windowHeight, - shouldRequestPassword: false, - isPasswordInvalid: false, isKeyboardOpen: false, }; - this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this); - this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this); - this.attemptPDFLoad = this.attemptPDFLoad.bind(this); this.toggleKeyboardOnSmallScreens = this.toggleKeyboardOnSmallScreens.bind(this); - this.calculatePageHeight = this.calculatePageHeight.bind(this); - this.calculatePageWidth = this.calculatePageWidth.bind(this); - this.renderPage = this.renderPage.bind(this); this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio); - this.setListAttributes = this.setListAttributes.bind(this); - const workerBlob = new Blob([pdfWorkerSource], {type: 'text/javascript'}); - pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); this.retrieveCanvasLimits(); } @@ -73,136 +44,6 @@ class PDFView extends Component { } } - /** - * Upon successful document load, combine an array of page viewports, - * set the number of pages on PDF, - * hide/reset PDF password form, and notify parent component that - * user input is no longer required. - * - * @param {Object} pdf - The PDF file instance - * @param {Number} pdf.numPages - Number of pages of the PDF file - * @param {Function} pdf.getPage - A method to get page by its number. It requires to have the context. It should be the pdf itself. - * @memberof PDFView - */ - onDocumentLoadSuccess(pdf) { - const {numPages} = pdf; - - Promise.all( - _.times(numPages, (index) => { - const pageNumber = index + 1; - - return pdf.getPage(pageNumber).then((page) => page.getViewport({scale: 1})); - }), - ).then((pageViewports) => { - this.setState({ - pageViewports, - numPages, - shouldRequestPassword: false, - isPasswordInvalid: false, - }); - }); - } - - /** - * Sets attributes to list container. - * It unblocks a default scroll by keyboard of browsers. - * @param {Object|undefined} ref - */ - setListAttributes(ref) { - if (!ref) { - return; - } - - // Useful for elements that should not be navigated to directly using the "Tab" key, - // but need to have keyboard focus set to them. - // eslint-disable-next-line no-param-reassign - ref.tabIndex = -1; - } - - /** - * Calculate the devicePixelRatio the page should be rendered with - * Each platform has a different default devicePixelRatio and different canvas limits, we need to verify that - * with the default devicePixelRatio it will be able to diplay the pdf correctly, if not we must change the devicePixelRatio. - * @param {Number} width of the page - * @param {Number} height of the page - * @returns {Number} devicePixelRatio for this page on this platform - */ - getDevicePixelRatio(width, height) { - const nbPixels = width * height; - const ratioHeight = this.props.maxCanvasHeight / height; - const ratioWidth = this.props.maxCanvasWidth / width; - const ratioArea = Math.sqrt(this.props.maxCanvasArea / nbPixels); - const ratio = Math.min(ratioHeight, ratioArea, ratioWidth); - return ratio > window.devicePixelRatio ? undefined : ratio; - } - - /** - * Calculates a proper page height. The method should be called only when there are page viewports. - * It is based on a ratio between the specific page viewport width and provided page width. - * Also, the app should take into account the page borders. - * @param {Number} pageIndex - * @returns {Number} - */ - calculatePageHeight(pageIndex) { - if (this.state.pageViewports.length === 0) { - Log.warn('Dev error: calculatePageHeight() in PDFView called too early'); - - return 0; - } - - const pageViewport = this.state.pageViewports[pageIndex]; - const pageWidth = this.calculatePageWidth(); - const scale = pageWidth / pageViewport.width; - const actualHeight = pageViewport.height * scale + PAGE_BORDER * 2; - - return actualHeight; - } - - /** - * Calculates a proper page width. - * It depends on a screen size. Also, the app should take into account the page borders. - * @returns {Number} - */ - calculatePageWidth() { - const pdfContainerWidth = this.state.containerWidth; - const pageWidthOnLargeScreen = Math.min(pdfContainerWidth - LARGE_SCREEN_SIDE_SPACING * 2, variables.pdfPageMaxWidth); - const pageWidth = this.props.isSmallScreenWidth ? this.state.containerWidth : pageWidthOnLargeScreen; - - return pageWidth + PAGE_BORDER * 2; - } - - /** - * Initiate password challenge process. The react-pdf/Document - * component calls this handler to indicate that a PDF requires a - * password, or to indicate that a previously provided password was - * invalid. - * - * The PasswordResponses constants used below were copied from react-pdf - * because they're not exported in entry.webpack. - * - * @param {Function} callback Callback used to send password to react-pdf - * @param {Number} reason Reason code for password request - */ - initiatePasswordChallenge(callback, reason) { - this.onPasswordCallback = callback; - - if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.NEED_PASSWORD) { - this.setState({shouldRequestPassword: true}); - } else if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.INCORRECT_PASSWORD) { - this.setState({shouldRequestPassword: true, isPasswordInvalid: true}); - } - } - - /** - * Send password to react-pdf via its callback so that it can attempt to load - * the PDF. - * - * @param {String} password Password to send via callback to react-pdf - */ - attemptPDFLoad(password) { - this.onPasswordCallback(password); - } - /** * On small screens notify parent that the keyboard has opened or closed. * @@ -233,92 +74,35 @@ class PDFView extends Component { } } - /** - * Render a specific page based on its index. - * The method includes a wrapper to apply virtualized styles. - * @param {Object} page item object of the List - * @param {Number} page.index index of the page - * @param {Object} page.style virtualized styles - * @returns {JSX.Element} - */ - renderPage({index, style}) { - const pageWidth = this.calculatePageWidth(); - const pageHeight = this.calculatePageHeight(index); - const devicePixelRatio = this.getDevicePixelRatio(pageWidth, pageHeight); - - return ( - - - - ); - } - renderPDFView() { const styles = this.props.themeStyles; - const pageWidth = this.calculatePageWidth(); const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter]; - // If we're requesting a password then we need to hide - but still render - - // the PDF component. - const pdfContainerStyle = this.state.shouldRequestPassword - ? [styles.PDFView, styles.noSelect, this.props.style, styles.invisible] - : [styles.PDFView, styles.noSelect, this.props.style]; - return ( - - this.setState({containerWidth: width, containerHeight: height})} - > - {this.props.translate('attachmentView.failedToLoadPDF')}} - loading={} - file={this.props.sourceURL} - options={{ - cMapUrl: 'cmaps/', - cMapPacked: true, - }} - externalLinkTarget="_blank" - onLoadSuccess={this.onDocumentLoadSuccess} - onPassword={this.initiatePasswordChallenge} - > - {this.state.pageViewports.length > 0 && ( - - {this.renderPage} - - )} - - - {this.state.shouldRequestPassword && ( - this.setState({isPasswordInvalid: false})} - isPasswordInvalid={this.state.isPasswordInvalid} - onPasswordFieldFocused={this.toggleKeyboardOnSmallScreens} - /> - )} + + } + ErrorComponent={{this.props.translate('attachmentView.failedToLoadPDF')}} + renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => ( + + )} + /> ); } diff --git a/src/styles/index.ts b/src/styles/index.ts index aececf93beb9..2f34c91d3fa6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2208,25 +2208,6 @@ const styles = (theme: ThemeColors) => backgroundColor: theme.modalBackground, }, - PDFView: { - // `display: grid` is not supported in native platforms! - // It's being used on Web/Desktop only to vertically center short PDFs, - // while preventing the overflow of the top of long PDF files. - ...display.dGrid, - width: '100%', - height: '100%', - justifyContent: 'center', - overflow: 'hidden', - alignItems: 'center', - }, - - PDFViewList: { - overflowX: 'hidden', - // There properties disable "focus" effect on list - boxShadow: 'none', - outline: 'none', - }, - getPDFPasswordFormStyle: (isSmallScreenWidth: boolean) => ({ width: isSmallScreenWidth ? '100%' : 350, From 2c725ca00033fbc698998ea06d64a96c612d83e8 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 21 Dec 2023 18:25:03 +0100 Subject: [PATCH 0037/1113] align password block --- src/styles/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/index.ts b/src/styles/index.ts index 1a029f2f4240..ad791838f125 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2205,6 +2205,7 @@ const styles = (theme: ThemeColors) => ({ width: isSmallScreenWidth ? '100%' : 350, ...(isSmallScreenWidth && flex.flex1), + alignSelf: 'flex-start', } satisfies ViewStyle), centeredModalStyles: (isSmallScreenWidth: boolean, isFullScreenWhenSmall: boolean) => From 183f9621101cb83b011f6cad5998f1ac4c325553 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 21 Dec 2023 19:20:45 +0100 Subject: [PATCH 0038/1113] align password form --- src/styles/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index ad791838f125..aefbd47f4fa4 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2203,8 +2203,8 @@ const styles = (theme: ThemeColors) => getPDFPasswordFormStyle: (isSmallScreenWidth: boolean) => ({ - width: isSmallScreenWidth ? '100%' : 350, - ...(isSmallScreenWidth && flex.flex1), + flexBasis: isSmallScreenWidth ? '100%' : 350, + flexGrow: 0, alignSelf: 'flex-start', } satisfies ViewStyle), From ee2b7794aae4533a72f124f180925b814819cadc Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Dec 2023 15:22:51 +0100 Subject: [PATCH 0039/1113] 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 0040/1113] 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 0041/1113] 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 0042/1113] 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 0043/1113] 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 0044/1113] 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 0045/1113] 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 0046/1113] 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 0047/1113] 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 0048/1113] 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 0049/1113] 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 0050/1113] 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 0051/1113] 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 0052/1113] 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 0053/1113] 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 0054/1113] 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 0055/1113] 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 0056/1113] 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 0057/1113] 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 0058/1113] 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 0059/1113] 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 0060/1113] 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 0061/1113] 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 0062/1113] 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 0063/1113] 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 0064/1113] 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 0065/1113] 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 0066/1113] 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 0067/1113] 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 0068/1113] 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 0069/1113] 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 0070/1113] 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 0071/1113] 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 0072/1113] 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 0073/1113] 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 0074/1113] 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 0075/1113] 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 0076/1113] 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 0077/1113] 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 0078/1113] 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 0079/1113] 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 0080/1113] 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 0081/1113] 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 0082/1113] 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 167775a4d69447f674cad364a891105e53b83e09 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 12 Jan 2024 14:19:45 +0700 Subject: [PATCH 0083/1113] Reapply changes --- .../request/step/IOURequestStepScan/index.js | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index c0c96826d124..3aa4235dea5d 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -1,6 +1,7 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useContext, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, Text, View} from 'react-native'; +import _ from 'underscore'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; import Shutter from '@assets/images/shutter.svg'; @@ -14,6 +15,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; +import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -74,6 +76,44 @@ function IOURequestStepScan({ const [isTorchAvailable, setIsTorchAvailable] = useState(false); const cameraRef = useRef(null); + const [videoConstraints, setVideoConstraints] = useState(null); + const tabIndex = 1; + const isScanTabActive = useTabNavigatorFocus({tabIndex}); + + /** + * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. + * The last deviceId is of regular len camera. + */ + useEffect(() => { + if (!_.isEmpty(videoConstraints) || !isScanTabActive || !Browser.isMobile()) { + return; + } + + navigator.mediaDevices.getUserMedia({video: true}).then((stream) => { + _.forEach(stream.getTracks(), (videoStream) => videoStream.stop()); + + if (!navigator.mediaDevices.enumerateDevices) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .filter((item) => item.kind === 'videoinput') + .last() + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + setVideoConstraints({deviceId: lastBackDeviceId}); + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isScanTabActive]); + const hideRecieptModal = () => { setIsAttachmentInvalid(false); }; @@ -211,18 +251,20 @@ function IOURequestStepScan({ {translate('receipt.cameraAccess')} )} - setCameraPermissionState('granted')} - onUserMediaError={() => setCameraPermissionState('denied')} - style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} - ref={cameraRef} - screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}}} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} - forceScreenshotSourceSize - cameraTabIndex={1} - /> + {!_.isEmpty(videoConstraints) && ( + setCameraPermissionState('granted')} + onUserMediaError={() => setCameraPermissionState('denied')} + style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} + ref={cameraRef} + screenshotFormat="image/png" + videoConstraints={videoConstraints} + torchOn={isFlashLightOn} + onTorchAvailability={setIsTorchAvailable} + forceScreenshotSourceSize + cameraTabIndex={tabIndex} + /> + )} From 6afd422e26ec35c5912b7b00945dc9c3d57e2804 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 12 Jan 2024 14:30:37 +0700 Subject: [PATCH 0084/1113] fix lint --- src/pages/iou/request/step/IOURequestStepScan/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 3aa4235dea5d..2a7c8be25950 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -78,14 +78,14 @@ function IOURequestStepScan({ const [videoConstraints, setVideoConstraints] = useState(null); const tabIndex = 1; - const isScanTabActive = useTabNavigatorFocus({tabIndex}); + const isTabActive = useTabNavigatorFocus({tabIndex}); /** * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. * The last deviceId is of regular len camera. */ useEffect(() => { - if (!_.isEmpty(videoConstraints) || !isScanTabActive || !Browser.isMobile()) { + if (!_.isEmpty(videoConstraints) || !isTabActive || !Browser.isMobile()) { return; } @@ -112,7 +112,7 @@ function IOURequestStepScan({ }); }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isScanTabActive]); + }, [isTabActive]); const hideRecieptModal = () => { setIsAttachmentInvalid(false); From d08dea532480085710e2acc190401eca7af9ff1c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 15:47:43 +0100 Subject: [PATCH 0085/1113] 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 0086/1113] 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 0087/1113] 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 0088/1113] 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 0089/1113] 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 0090/1113] 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 0091/1113] 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 0092/1113] 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 0093/1113] 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 0094/1113] 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 0095/1113] 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 0096/1113] 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 0097/1113] 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 0098/1113] 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 0099/1113] 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 0100/1113] 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 0101/1113] 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 0102/1113] 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 0103/1113] 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 0104/1113] 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 0105/1113] 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 0106/1113] 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 d95ba1dc8ceee16866056e5f806e308aa0cb9460 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 11:43:06 +0700 Subject: [PATCH 0107/1113] fix: Inconsistency of flashlight/torch behavior in Scan tab --- .../NavigationAwareCamera/index.js | 47 +------------------ .../request/step/IOURequestStepScan/index.js | 30 ++++++++++-- 2 files changed, 28 insertions(+), 49 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js index 10b16da13b6e..37223915f4a2 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js @@ -1,61 +1,20 @@ import PropTypes from 'prop-types'; -import React, {useEffect, useRef} from 'react'; +import React from 'react'; import {View} from 'react-native'; import Webcam from 'react-webcam'; import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; const propTypes = { - /** Flag to turn on/off the torch/flashlight - if available */ - torchOn: PropTypes.bool, - /** The index of the tab that contains this camera */ cameraTabIndex: PropTypes.number.isRequired, - - /** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */ - onUserMedia: PropTypes.func, - - /** Callback function passing torch/flashlight capability as bool param of the browser */ - onTorchAvailability: PropTypes.func, -}; - -const defaultProps = { - onUserMedia: undefined, - onTorchAvailability: undefined, - torchOn: false, }; // Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, cameraTabIndex, ...props}, ref) => { - const trackRef = useRef(null); +const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => { const shouldShowCamera = useTabNavigatorFocus({ tabIndex: cameraTabIndex, }); - const handleOnUserMedia = (stream) => { - if (props.onUserMedia) { - props.onUserMedia(stream); - } - - const [track] = stream.getVideoTracks(); - const capabilities = track.getCapabilities(); - if (capabilities.torch) { - trackRef.current = track; - } - if (onTorchAvailability) { - onTorchAvailability(!!capabilities.torch); - } - }; - - useEffect(() => { - if (!trackRef.current) { - return; - } - - trackRef.current.applyConstraints({ - advanced: [{torch: torchOn}], - }); - }, [torchOn]); - if (!shouldShowCamera) { return null; } @@ -67,7 +26,6 @@ const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, c // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - onUserMedia={handleOnUserMedia} /> ); @@ -75,6 +33,5 @@ const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, c NavigationAwareCamera.propTypes = propTypes; NavigationAwareCamera.displayName = 'NavigationAwareCamera'; -NavigationAwareCamera.defaultProps = defaultProps; export default NavigationAwareCamera; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7c6efca4a32f..c2e9882d5288 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -74,6 +74,7 @@ function IOURequestStepScan({ const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(false); const cameraRef = useRef(null); + const trackRef = useRef(null); const hideRecieptModal = () => { setIsAttachmentInvalid(false); @@ -162,11 +163,34 @@ function IOURequestStepScan({ navigateToConfirmationStep(); }; + const handleOnUserMedia = (stream) => { + setCameraPermissionState('granted'); + + const [track] = stream.getVideoTracks(); + const capabilities = track.getCapabilities(); + if (capabilities.torch) { + trackRef.current = track; + } + setIsTorchAvailable(!!capabilities.torch); + }; + const capturePhoto = useCallback(() => { if (!cameraRef.current.getScreenshot) { return; } + if (trackRef.current && isFlashLightOn) { + trackRef.current.applyConstraints({ + advanced: [{torch: true}], + }); + } const imageBase64 = cameraRef.current.getScreenshot(); + + if (trackRef.current && isFlashLightOn) { + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); + } + const filename = `receipt_${Date.now()}.png`; const file = FileUtils.base64ToFile(imageBase64, filename); const source = URL.createObjectURL(file); @@ -178,7 +202,7 @@ function IOURequestStepScan({ } navigateToConfirmationStep(); - }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep]); + }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep, isFlashLightOn]); const panResponder = useRef( PanResponder.create({ @@ -209,14 +233,12 @@ function IOURequestStepScan({ )} setCameraPermissionState('granted')} + onUserMedia={handleOnUserMedia} onUserMediaError={() => setCameraPermissionState('denied')} style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" videoConstraints={{facingMode: {exact: 'environment'}}} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} forceScreenshotSourceSize cameraTabIndex={1} /> From 825a5439fad4d931ac32fb9d9a4b4c4331e3528b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 16:54:20 +0100 Subject: [PATCH 0108/1113] 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 0109/1113] 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 0110/1113] 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 bd5f17703af96409056a743804362e7f7efbf149 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 24 Nov 2023 16:07:50 -0800 Subject: [PATCH 0111/1113] Include comment in report name for amounts owing --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/types.ts | 2 +- src/libs/ReportUtils.ts | 38 +++++++++++++++++++++++--------------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..87b12f631631 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -601,7 +601,7 @@ export default { splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} each`, - payerOwesAmount: ({payer, amount}: PayerOwesAmountParams) => `${payer} owes ${amount}`, + payerOwesAmount: ({payer, amount, comment}: PayerOwesAmountParams) => `${payer} owes ${amount}${comment ? ` for ${comment}` : ''}`, payerOwes: ({payer}: PayerOwesParams) => `${payer} owes: `, payerPaidAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer ? `${payer} ` : ''}paid ${amount}`, payerPaid: ({payer}: PayerPaidParams) => `${payer} paid: `, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..2c7e58655ca8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -594,7 +594,7 @@ export default { splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`, - payerOwesAmount: ({payer, amount}: PayerOwesAmountParams) => `${payer} debe ${amount}`, + payerOwesAmount: ({payer, amount, comment}: PayerOwesAmountParams) => `${payer} debe ${amount}${comment ? ` para ${comment}` : ''}`, payerOwes: ({payer}: PayerOwesParams) => `${payer} debe: `, payerPaidAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer ? `${payer} ` : ''}pagó ${amount}`, payerPaid: ({payer}: PayerPaidParams) => `${payer} pagó: `, diff --git a/src/languages/types.ts b/src/languages/types.ts index 3185b7a8f6f1..eab9991b73d8 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -115,7 +115,7 @@ type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; type AmountEachParams = {amount: number}; -type PayerOwesAmountParams = {payer: string; amount: number | string}; +type PayerOwesAmountParams = {payer: string; amount: number | string; comment?: string}; type PayerOwesParams = {payer: string}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d48567ebdaf3..32fb047d9126 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2178,26 +2178,28 @@ function getReportPreviewMessage( return reportActionMessage; } + let linkedTransaction: Transaction | EmptyObject = {}; + if (!isEmptyObject(reportAction)) { + linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + } + if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action - const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } - if (!isEmptyObject(linkedTransaction)) { - if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { - return Localize.translateLocal('iou.receiptScanning'); - } - - if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { - return Localize.translateLocal('iou.receiptMissingDetails'); - } + if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } - const transactionDetails = getTransactionDetails(linkedTransaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); - return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { + return Localize.translateLocal('iou.receiptMissingDetails'); } + + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); } const totalAmount = getMoneyRequestReimbursableTotal(report); @@ -2214,8 +2216,6 @@ function getReportPreviewMessage( } if (!isEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { - const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -2261,7 +2261,15 @@ function getReportPreviewMessage( return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay})}`; } - return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); + if (containsNonReimbursable) { + return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); + } + + if (!isEmptyObject(linkedTransaction)) { + const comment = TransactionUtils.getDescription(linkedTransaction); + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); + } + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); } /** From ee2c15d47495cc7a81ffc8e52319cd0bcd817fdc Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 09:30:14 -0800 Subject: [PATCH 0112/1113] Reuse translateLocal call --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 32fb047d9126..4cdcdc863aa3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2265,11 +2265,11 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } + let comment: string | undefined if (!isEmptyObject(linkedTransaction)) { - const comment = TransactionUtils.getDescription(linkedTransaction); - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); + comment = TransactionUtils.getDescription(linkedTransaction); } - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } /** From eeca194eaaddc0ef1ae75312d29b4552e8024aab Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 10:29:53 -0800 Subject: [PATCH 0113/1113] Remove redundant type declaration --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4cdcdc863aa3..d1d38bbff305 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2265,7 +2265,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } - let comment: string | undefined + let comment if (!isEmptyObject(linkedTransaction)) { comment = TransactionUtils.getDescription(linkedTransaction); } From db0d8d1ecc0436fbba67479a230a9de0809850f4 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 10:51:27 -0800 Subject: [PATCH 0114/1113] Avoid variable reassignments --- src/libs/ReportUtils.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d1d38bbff305..e57dff6b4ae2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2178,11 +2178,7 @@ function getReportPreviewMessage( return reportActionMessage; } - let linkedTransaction: Transaction | EmptyObject = {}; - if (!isEmptyObject(reportAction)) { - linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - } - + const linkedTransaction = !isEmptyObject(reportAction) ? TransactionUtils.getLinkedTransaction(reportAction) : {}; if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action if (isEmptyObject(linkedTransaction)) { @@ -2265,10 +2261,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } - let comment - if (!isEmptyObject(linkedTransaction)) { - comment = TransactionUtils.getDescription(linkedTransaction); - } + const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } From c2731c056e804b06b93cbe63c0ed79f9ab8d9ea6 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 10:52:31 -0800 Subject: [PATCH 0115/1113] Remove unrelated code change --- src/libs/ReportUtils.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e57dff6b4ae2..ee85c1aca958 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2185,17 +2185,19 @@ function getReportPreviewMessage( return reportActionMessage; } - if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { - return Localize.translateLocal('iou.receiptScanning'); - } + if (!isEmptyObject(linkedTransaction)) { + if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } - if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { - return Localize.translateLocal('iou.receiptMissingDetails'); - } + if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { + return Localize.translateLocal('iou.receiptMissingDetails'); + } - const transactionDetails = getTransactionDetails(linkedTransaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); - return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + } } const totalAmount = getMoneyRequestReimbursableTotal(report); From 036ac5c5a87fb72784b60fb899c656d346b29a74 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 14:51:11 -0800 Subject: [PATCH 0116/1113] Update editRegularMoneyTransaction --- src/libs/actions/IOU.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ee752a1f0ef..410da027351a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2272,10 +2272,16 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // Update the last message of the chat report const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { - payer: ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '', - amount: CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency), - }); + const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '' + const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency) + let messageText + if (hasNonReimbursableTransactions) { + messageText = Localize.translateLocal('iou.payerSpentAmount', { payer, amount: formattedAmount }); + } else { + const comment = TransactionUtils.getDescription(updatedTransaction) + messageText = Localize.translateLocal('iou.payerOwesAmount', { payer, amount: formattedAmount, comment }); + } + updatedChatReport.lastMessageText = messageText; updatedChatReport.lastMessageHtml = messageText; } From 40073f04b4eed15987aa815497b75776cefc2e5b Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 16:00:57 -0800 Subject: [PATCH 0117/1113] Include request description when request deleted --- src/libs/actions/IOU.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 410da027351a..f861a37b1051 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -28,6 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import * as Policy from './Policy'; import * as Report from './Report'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; let betas; Onyx.connect({ @@ -2569,10 +2570,16 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { - payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', - amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), - }); + const payer = ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || ''; + const formattedAmount = CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency); + let messageText + if (hasNonReimbursableTransactions) { + messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}) + } else { + const comment = !isEmptyObject(transaction) ? TransactionUtils.getDescription(transaction) : undefined; + messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}) + } + updatedReportPreviewAction.message[0].text = messageText; updatedReportPreviewAction.message[0].html = messageText; From f7032dd759854ce6da538e79dd855894814c9b12 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 16:03:13 -0800 Subject: [PATCH 0118/1113] Include IOU description in report name --- src/libs/ReportUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ee85c1aca958..2928e54a79ee 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1928,7 +1928,9 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< } if (isProcessingReport(report) || isDraftExpenseReport(report) || moneyRequestTotal === 0) { - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}); + const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : [] + const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount, comment}); } return payerPaidAmountMessage; From 2e2ac1f645337fa4d5255fc2fbfe350384a2571a Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 16:06:50 -0800 Subject: [PATCH 0119/1113] Run prettier and lint --- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/IOU.js | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2928e54a79ee..004488d3a151 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1928,8 +1928,8 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< } if (isProcessingReport(report) || isDraftExpenseReport(report) || moneyRequestTotal === 0) { - const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : [] - const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined + const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : []; + const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined; return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount, comment}); } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f861a37b1051..e7a579d7b671 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -26,9 +26,9 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as Policy from './Policy'; import * as Report from './Report'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; let betas; Onyx.connect({ @@ -2273,14 +2273,14 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // Update the last message of the chat report const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '' - const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency) - let messageText + const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || ''; + const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency); + let messageText; if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', { payer, amount: formattedAmount }); + messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); } else { - const comment = TransactionUtils.getDescription(updatedTransaction) - messageText = Localize.translateLocal('iou.payerOwesAmount', { payer, amount: formattedAmount, comment }); + const comment = TransactionUtils.getDescription(updatedTransaction); + messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); } updatedChatReport.lastMessageText = messageText; @@ -2572,12 +2572,12 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); const payer = ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || ''; const formattedAmount = CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency); - let messageText + let messageText; if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}) + messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); } else { const comment = !isEmptyObject(transaction) ? TransactionUtils.getDescription(transaction) : undefined; - messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}) + messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); } updatedReportPreviewAction.message[0].text = messageText; From c1c260fb20dad42c5ee43b84a80cdcc1686dd5df Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 20 Jan 2024 18:15:31 +0100 Subject: [PATCH 0120/1113] 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 c08d9057304624e415ec9fd006e6c6df3c75aa94 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 15:35:55 +0700 Subject: [PATCH 0121/1113] add settimeout --- src/CONST.ts | 1 + .../request/step/IOURequestStepScan/index.js | 39 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0b10e5767328..264810572030 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -47,6 +47,7 @@ const CONST = { OUT: 'out', }, ARROW_HIDE_DELAY: 3000, + TORCH_EFFECT: 1000, API_ATTACHMENT_VALIDATIONS: { // 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index c2e9882d5288..035db5dbb5a1 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -174,23 +174,9 @@ function IOURequestStepScan({ setIsTorchAvailable(!!capabilities.torch); }; - const capturePhoto = useCallback(() => { - if (!cameraRef.current.getScreenshot) { - return; - } - if (trackRef.current && isFlashLightOn) { - trackRef.current.applyConstraints({ - advanced: [{torch: true}], - }); - } + const getScreenshot = useCallback(() => { const imageBase64 = cameraRef.current.getScreenshot(); - if (trackRef.current && isFlashLightOn) { - trackRef.current.applyConstraints({ - advanced: [{torch: false}], - }); - } - const filename = `receipt_${Date.now()}.png`; const file = FileUtils.base64ToFile(imageBase64, filename); const source = URL.createObjectURL(file); @@ -202,7 +188,28 @@ function IOURequestStepScan({ } navigateToConfirmationStep(); - }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep, isFlashLightOn]); + }, [action, transactionID, updateScanAndNavigate, navigateToConfirmationStep]); + + const capturePhoto = useCallback(() => { + if (!cameraRef.current.getScreenshot) { + return; + } + + if (trackRef.current && isFlashLightOn) { + trackRef.current.applyConstraints({ + advanced: [{torch: true}], + }); + setTimeout(() => { + getScreenshot(); + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); + }, CONST.TORCH_EFFECT); + return; + } + + getScreenshot(); + }, [cameraRef, isFlashLightOn, getScreenshot]); const panResponder = useRef( PanResponder.create({ From 14ab0d60bebe946ac1a14de387fdefbf4ddd7b3c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 23 Jan 2024 14:41:20 +0100 Subject: [PATCH 0122/1113] 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:35:30 +0700 Subject: [PATCH 0123/1113] remove settimeout --- src/CONST.ts | 2 -- .../request/step/IOURequestStepScan/index.js | 17 +++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 264810572030..ae5fbed6dafc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -47,8 +47,6 @@ const CONST = { OUT: 'out', }, ARROW_HIDE_DELAY: 3000, - TORCH_EFFECT: 1000, - API_ATTACHMENT_VALIDATIONS: { // 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion MAX_SIZE: 25165824, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 035db5dbb5a1..cd262ff24906 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -196,15 +196,16 @@ function IOURequestStepScan({ } if (trackRef.current && isFlashLightOn) { - trackRef.current.applyConstraints({ - advanced: [{torch: true}], - }); - setTimeout(() => { - getScreenshot(); - trackRef.current.applyConstraints({ - advanced: [{torch: false}], + trackRef.current + .applyConstraints({ + advanced: [{torch: true}], + }) + .then(() => { + getScreenshot(); + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); }); - }, CONST.TORCH_EFFECT); return; } From f5f2327a74ccaeca6ba35f85bee9024055172c82 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 24 Jan 2024 16:42:32 +0100 Subject: [PATCH 0124/1113] 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 0125/1113] 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 0126/1113] 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 719d63d473ecb93ecc8894b9598c47bbae4d7158 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 26 Jan 2024 10:29:54 -0800 Subject: [PATCH 0127/1113] Undo changes to text outside LHN preview --- src/libs/ReportUtils.ts | 4 +--- src/libs/actions/IOU.js | 29 ++++++++--------------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ac1686f88544..edcd57078e0c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1931,9 +1931,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< } if (isProcessingReport(report) || isDraftExpenseReport(report) || moneyRequestTotal === 0) { - const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : []; - const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined; - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount, comment}); + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}); } return payerPaidAmountMessage; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index e7a579d7b671..7ee752a1f0ef 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -26,7 +26,6 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as Policy from './Policy'; import * as Report from './Report'; @@ -2273,16 +2272,10 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // Update the last message of the chat report const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || ''; - const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency); - let messageText; - if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); - } else { - const comment = TransactionUtils.getDescription(updatedTransaction); - messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); - } - + const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { + payer: ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '', + amount: CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency), + }); updatedChatReport.lastMessageText = messageText; updatedChatReport.lastMessageHtml = messageText; } @@ -2570,16 +2563,10 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const payer = ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || ''; - const formattedAmount = CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency); - let messageText; - if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); - } else { - const comment = !isEmptyObject(transaction) ? TransactionUtils.getDescription(transaction) : undefined; - messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); - } - + const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { + payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', + amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), + }); updatedReportPreviewAction.message[0].text = messageText; updatedReportPreviewAction.message[0].html = messageText; From e90e0085e5e300fd841c6cf4cfafd7d56b71f288 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 29 Jan 2024 16:04:33 +0700 Subject: [PATCH 0128/1113] remove NewDistanceRequestPage and EditRequestDistancePage --- src/ROUTES.ts | 10 +- .../MoneyRequestConfirmationList.js | 12 -- ...oraryForRefactorRequestConfirmationList.js | 10 +- .../ReportActionItem/MoneyRequestView.js | 12 +- .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig.ts | 1 - src/libs/Navigation/types.ts | 5 +- src/pages/EditRequestDistancePage.js | 122 ------------------ src/pages/EditRequestPage.js | 11 -- src/pages/iou/MoneyRequestSelectorPage.js | 12 -- src/pages/iou/NewDistanceRequestPage.js | 85 ------------ 11 files changed, 27 insertions(+), 254 deletions(-) delete mode 100644 src/pages/EditRequestDistancePage.js delete mode 100644 src/pages/iou/NewDistanceRequestPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index deabdc0ac853..b985f993367a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -295,10 +295,6 @@ const ROUTES = { route: ':iouType/new/receipt/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const, }, - MONEY_REQUEST_DISTANCE: { - route: ':iouType/new/address/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const, - }, MONEY_REQUEST_DISTANCE_TAB: { route: ':iouType/new/:reportID?/distance', getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const, @@ -350,9 +346,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { - route: 'create/:iouType/distance/:transactionID/:reportID', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/distance/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/distance/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_MERCHANT: { route: 'create/:iouType/merchant/:transactionID/:reportID', diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index d967d04ab94b..101e135d36e1 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -703,18 +703,6 @@ function MoneyRequestConfirmationList(props) { error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} /> )} - {props.isDistanceRequest && ( - Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - disabled={didConfirm || !isTypeRequest} - interactive={!props.isReadOnly} - /> - )} {shouldShowMerchant && ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())) + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute( + CONST.IOU.ACTION.CREATE, + CONST.IOU.TYPE.REQUEST, + transaction.transactionID, + reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ) } disabled={didConfirm || !isTypeRequest} interactive={!isReadOnly} diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 3121328138ee..6cc2119a9044 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -301,7 +301,17 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate interactive={canEditDistance} shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + onPress={() => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute( + CONST.IOU.ACTION.EDIT, + CONST.IOU.TYPE.REQUEST, + transaction.transactionID, + report.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ) + } /> ) : ( diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 3a843e400409..9c2361916934 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -103,7 +103,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../pages/iou/MoneyRequestWaypointPage').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.DISTANCE]: () => require('../../../pages/iou/NewDistanceRequestPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.RECEIPT]: () => require('../../../pages/EditRequestReceiptPage').default as React.ComponentType, }); diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index d4e04d5402e2..715b14c4cb90 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -435,7 +435,6 @@ const linkingConfig: LinkingOptions = { [SCREENS.MONEY_REQUEST.TAG]: ROUTES.MONEY_REQUEST_TAG.route, [SCREENS.MONEY_REQUEST.MERCHANT]: ROUTES.MONEY_REQUEST_MERCHANT.route, [SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route, - [SCREENS.MONEY_REQUEST.DISTANCE]: ROUTES.MONEY_REQUEST_DISTANCE.route, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT, [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b4a77f96cc74..ea2b48df4ed2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -238,9 +238,12 @@ type MoneyRequestNavigatorParamList = { waypointIndex: string; threadReportID: number; }; - [SCREENS.MONEY_REQUEST.DISTANCE]: { + [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: { + action: string; iouType: ValueOf; + transactionID: string; reportID: string; + backTo: string; }; [SCREENS.MONEY_REQUEST.RECEIPT]: { iouType: string; diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js deleted file mode 100644 index f3ea76a3390a..000000000000 --- a/src/pages/EditRequestDistancePage.js +++ /dev/null @@ -1,122 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useEffect, useRef} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import DistanceRequest from '@components/DistanceRequest'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePrevious from '@hooks/usePrevious'; -import Navigation from '@libs/Navigation/Navigation'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import reportPropTypes from './reportPropTypes'; - -const propTypes = { - /** The transactionID we're currently editing */ - transactionID: PropTypes.string.isRequired, - - /** The report to with which the distance request is associated */ - report: reportPropTypes.isRequired, - - /** Passed from the navigator */ - route: PropTypes.shape({ - /** Parameters the route gets */ - params: PropTypes.shape({ - /** Type of IOU */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)), - - /** Id of the report on which the distance request is being created */ - reportID: PropTypes.string, - }), - }).isRequired, - - /* Onyx props */ - /** The original transaction that is being edited */ - transaction: transactionPropTypes, - - /** backup version of the original transaction */ - transactionBackup: transactionPropTypes, -}; - -const defaultProps = { - transaction: {}, - transactionBackup: {}, -}; - -function EditRequestDistancePage({report, route, transaction, transactionBackup}) { - const {isOffline} = useNetwork(); - const {translate} = useLocalize(); - const hasWaypointError = useRef(false); - const prevIsLoading = usePrevious(transaction.isLoading); - - useEffect(() => { - hasWaypointError.current = Boolean(lodashGet(transaction, 'errorFields.route') || lodashGet(transaction, 'errorFields.waypoints')); - - // When the loading goes from true to false, then we know the transaction has just been - // saved to the server. Check for errors. If there are no errors, then the modal can be closed. - if (prevIsLoading && !transaction.isLoading && !hasWaypointError.current) { - Navigation.dismissModal(report.reportID); - } - }, [transaction, prevIsLoading, report]); - - /** - * Save the changes to the original transaction object - * @param {Object} waypoints - */ - const saveTransaction = (waypoints) => { - // If nothing was changed, simply go to transaction thread - // We compare only addresses because numbers are rounded while backup - const oldWaypoints = lodashGet(transactionBackup, 'comment.waypoints', {}); - const oldAddresses = _.mapObject(oldWaypoints, (waypoint) => _.pick(waypoint, 'address')); - const addresses = _.mapObject(waypoints, (waypoint) => _.pick(waypoint, 'address')); - if (_.isEqual(oldAddresses, addresses)) { - Navigation.dismissModal(report.reportID); - return; - } - - IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints); - - // If the client is offline, then the modal can be closed as well (because there are no errors or other feedback to show them - // until they come online again and sync with the server). - if (isOffline) { - Navigation.dismissModal(report.reportID); - } - }; - - return ( - - Navigation.goBack()} - /> - - - ); -} - -EditRequestDistancePage.propTypes = propTypes; -EditRequestDistancePage.defaultProps = defaultProps; -EditRequestDistancePage.displayName = 'EditRequestDistancePage'; -export default withOnyx({ - transaction: { - key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION}${props.transactionID}`, - }, - transactionBackup: { - key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${props.transactionID}`, - }, -})(EditRequestDistancePage); diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 3eb9d88f1120..9f1ba51806e5 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -23,7 +23,6 @@ import EditRequestAmountPage from './EditRequestAmountPage'; import EditRequestCategoryPage from './EditRequestCategoryPage'; import EditRequestCreatedPage from './EditRequestCreatedPage'; import EditRequestDescriptionPage from './EditRequestDescriptionPage'; -import EditRequestDistancePage from './EditRequestDistancePage'; import EditRequestMerchantPage from './EditRequestMerchantPage'; import EditRequestReceiptPage from './EditRequestReceiptPage'; import EditRequestTagPage from './EditRequestTagPage'; @@ -264,16 +263,6 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep ); } - if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DISTANCE) { - return ( - - ); - } - return ( { const moneyRequestID = `${iouType}${reportID}`; @@ -133,13 +128,6 @@ function MoneyRequestSelectorPage(props) { initialParams={{reportID, iouType}} /> {() => } - {shouldDisplayDistanceRequest && ( - - )} ) : ( diff --git a/src/pages/iou/NewDistanceRequestPage.js b/src/pages/iou/NewDistanceRequestPage.js deleted file mode 100644 index 750ac5d0141e..000000000000 --- a/src/pages/iou/NewDistanceRequestPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import DistanceRequest from '@components/DistanceRequest'; -import Navigation from '@libs/Navigation/Navigation'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import {iouPropTypes} from './propTypes'; - -const propTypes = { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The report on which the request is initiated on */ - report: reportPropTypes, - - /** Passed from the navigator */ - route: PropTypes.shape({ - /** Parameters the route gets */ - params: PropTypes.shape({ - /** Type of IOU */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)), - /** Id of the report on which the distance request is being created */ - reportID: PropTypes.string, - }), - }), -}; - -const defaultProps = { - iou: {}, - report: {}, - route: { - params: { - iouType: '', - reportID: '', - }, - }, -}; - -// This component is responsible for getting the transactionID from the IOU key, or creating the transaction if it doesn't exist yet, and then passing the transactionID. -// You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that DistanceRequest can subscribe to the transaction. -function NewDistanceRequestPage({iou, report, route}) { - const iouType = lodashGet(route, 'params.iouType', 'request'); - const isEditingNewRequest = Navigation.getActiveRoute().includes('address'); - - useEffect(() => { - if (iou.transactionID) { - return; - } - IOU.setUpDistanceTransaction(); - }, [iou.transactionID]); - - const onSubmit = useCallback(() => { - if (isEditingNewRequest) { - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report.reportID)); - return; - } - IOU.navigateToNextPage(iou, iouType, report); - }, [iou, iouType, isEditingNewRequest, report]); - - return ( - - ); -} - -NewDistanceRequestPage.displayName = 'NewDistanceRequestPage'; -NewDistanceRequestPage.propTypes = propTypes; -NewDistanceRequestPage.defaultProps = defaultProps; -export default withOnyx({ - iou: {key: ONYXKEYS.IOU}, - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID')}`, - }, -})(NewDistanceRequestPage); From 4f4a2c21d112e39a41b2339acc0046893114a83c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 29 Jan 2024 12:14:43 +0100 Subject: [PATCH 0129/1113] 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 397d797f7dbe15f8f6745848787b4a89aeccd404 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 29 Jan 2024 18:15:24 +0700 Subject: [PATCH 0130/1113] add pendingAccounts to report --- src/languages/en.ts | 4 +++ src/languages/es.ts | 4 +++ src/libs/actions/Report.ts | 50 +++++++++++++++++++++++++++++------- src/pages/RoomMembersPage.js | 3 ++- src/types/onyx/Report.ts | 9 ++++++- 5 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 0363198c5007..4958286a1d0a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1882,6 +1882,10 @@ export default { genericCreateReportFailureMessage: 'Unexpected error creating this chat, please try again later', genericAddCommentFailureMessage: 'Unexpected error while posting the comment, please try again later', noActivityYet: 'No activity yet', + people: { + genericAdd: 'There was a problem adding this room member.', + genericRemove: 'There was a problem removing that room member.', + }, }, chronos: { oooEventSummaryFullDay: ({summary, dayCount, date}: OOOEventSummaryFullDayParams) => `${summary} for ${dayCount} ${dayCount === 1 ? 'day' : 'days'} until ${date}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 5fb65ab42d50..0f38c7b110d8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1908,6 +1908,10 @@ export default { genericCreateReportFailureMessage: 'Error inesperado al crear el chat. Por favor, inténtalo más tarde', genericAddCommentFailureMessage: 'Error inesperado al añadir el comentario. Por favor, inténtalo más tarde', noActivityYet: 'Sin actividad todavía', + people: { + genericAdd: '', + genericRemove: '', + }, }, chronos: { oooEventSummaryFullDay: ({summary, dayCount, date}: OOOEventSummaryFullDayParams) => `${summary} por ${dayCount} ${dayCount === 1 ? 'día' : 'días'} hasta el ${date}`, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 6222c09a898e..38c1fe4429b4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -7,6 +7,7 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {NullishDeep} from 'react-native-onyx/lib/types'; import type {PartialDeep, ValueOf} from 'type-fest'; +import * as _ from 'underscore'; import type {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; @@ -65,8 +66,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; +import {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; -import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report'; +import type {NotificationPreference, PendingAccount, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -2145,6 +2147,18 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record OptionsListUtils.addSMSDomainIfPhoneNumber(memberLogin)); const newPersonalDetailsOnyxData = PersonalDetailsUtils.getNewPersonalDetailsOnyxData(logins, inviteeAccountIDs); + const optimisticPendingAccounts: Record = {}; + const successPendingAccounts: Record = {}; + const failurePendingAccounts: Record = {}; + + inviteeAccountIDs.forEach((accountID) => { + optimisticPendingAccounts[accountID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}; + successPendingAccounts[accountID] = {pendingAction: null}; + failurePendingAccounts[accountID] = { + errors: ErrorUtils.getMicroSecondOnyxError('report.people.error.genericAdd'), + }; + }); + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2152,12 +2166,22 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record !targetAccountIDs.includes(id)); - const visibleChatMemberAccountIDsAfterRemoval = report?.visibleChatMemberAccountIDs?.filter((id: number) => !targetAccountIDs.includes(id)); + const optimisticPendingAccounts: Record = {}; + const successPendingAccounts: Record = {}; + const failurePendingAccounts: Record = {}; + + targetAccountIDs.forEach((accountID) => { + optimisticPendingAccounts[accountID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}; + successPendingAccounts[accountID] = {pendingAction: null}; + failurePendingAccounts[accountID] = { + errors: ErrorUtils.getMicroSecondOnyxError('report.people.error.genericRemove'), + }; + }); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - participantAccountIDs: participantAccountIDsAfterRemoval, - visibleChatMemberAccountIDs: visibleChatMemberAccountIDsAfterRemoval, + pendingAccounts: optimisticPendingAccounts, }, }, ]; @@ -2203,7 +2236,7 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { participantAccountIDs: report?.participantAccountIDs, - visibleChatMemberAccountIDs: report?.visibleChatMemberAccountIDs, + pendingAccounts: failurePendingAccounts, }, }, ]; @@ -2215,8 +2248,7 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - participantAccountIDs: participantAccountIDsAfterRemoval, - visibleChatMemberAccountIDs: visibleChatMemberAccountIDsAfterRemoval, + pendingAccounts: successPendingAccounts, }, }, ]; diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js index 30ffd60aa4ac..0ae0b74242ae 100644 --- a/src/pages/RoomMembersPage.js +++ b/src/pages/RoomMembersPage.js @@ -173,6 +173,7 @@ function RoomMembersPage(props) { const getMemberOptions = () => { let result = []; + const pendingAccounts = props.report.pendingAccounts; _.each(props.report.visibleChatMemberAccountIDs, (accountID) => { const details = personalDetails[accountID]; @@ -220,9 +221,9 @@ function RoomMembersPage(props) { type: CONST.ICON_TYPE_AVATAR, }, ], + pendingAction: _.get(pendingAccounts, [accountID, 'pendingAction']), }); }); - result = _.sortBy(result, (value) => value.text.toLowerCase()); return result; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index f3b20c68038e..a3833ca7a529 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -20,6 +20,11 @@ type Participant = { type Participants = Record; +type PendingAccount = { + errors?: OnyxCommon.Errors; + pendingAction?: OnyxCommon.PendingAction | null; +}; + type Report = { /** The specific type of chat */ chatType?: ValueOf; @@ -171,8 +176,10 @@ type Report = { /** If the report contains reportFields, save the field id and its value */ reportFields?: Record; + + pendingAccounts?: Record; }; export default Report; -export type {NotificationPreference, WriteCapability, Note}; +export type {NotificationPreference, WriteCapability, Note, PendingAccount}; From 48824e545c15ae0e42bac7d66b43e4b81a3c2b48 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 29 Jan 2024 12:50:43 +0100 Subject: [PATCH 0131/1113] 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 e42a6e28668711beb76803e7181259051be9eaa9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 30 Jan 2024 11:05:50 +0700 Subject: [PATCH 0132/1113] clear error --- src/languages/en.ts | 6 ++++-- src/languages/es.ts | 6 ++++-- src/libs/actions/Report.ts | 39 +++++++++++++++++++++++++++++++----- src/pages/RoomMembersPage.js | 19 +++++++++++++++++- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 4958286a1d0a..3d3aeeb4b351 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1883,8 +1883,10 @@ export default { genericAddCommentFailureMessage: 'Unexpected error while posting the comment, please try again later', noActivityYet: 'No activity yet', people: { - genericAdd: 'There was a problem adding this room member.', - genericRemove: 'There was a problem removing that room member.', + error: { + genericAdd: 'There was a problem adding this room member.', + genericRemove: 'There was a problem removing that room member.', + }, }, }, chronos: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 0f38c7b110d8..ce7f9ab392db 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1909,8 +1909,10 @@ export default { genericAddCommentFailureMessage: 'Error inesperado al añadir el comentario. Por favor, inténtalo más tarde', noActivityYet: 'Sin actividad todavía', people: { - genericAdd: '', - genericRemove: '', + error: { + genericAdd: '', + genericRemove: '', + }, }, }, chronos: { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 38c1fe4429b4..90c7f7525082 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -7,7 +7,6 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {NullishDeep} from 'react-native-onyx/lib/types'; import type {PartialDeep, ValueOf} from 'type-fest'; -import * as _ from 'underscore'; import type {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; @@ -66,7 +65,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; -import {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, PendingAccount, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; @@ -2188,8 +2186,6 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record !targetAccountIDs.includes(id)); + const visibleChatMemberAccountIDsAfterRemoval = report?.visibleChatMemberAccountIDs?.filter((id: number) => !targetAccountIDs.includes(id)); const optimisticPendingAccounts: Record = {}; const successPendingAccounts: Record = {}; @@ -2235,7 +2233,6 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - participantAccountIDs: report?.participantAccountIDs, pendingAccounts: failurePendingAccounts, }, }, @@ -2248,6 +2245,8 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { + participantAccountIDs: participantAccountIDsAfterRemoval, + visibleChatMemberAccountIDs: visibleChatMemberAccountIDsAfterRemoval, pendingAccounts: successPendingAccounts, }, }, @@ -2691,6 +2690,34 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt API.write(WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER, parameters, {optimisticData, failureData}); } +/** + * Removes an error after trying to delete a member + */ +function clearDeleteMemberError(reportID: string, accountID: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { + pendingAccounts: { + [accountID]: null, + }, + }); +} + +/** + * Removes an error after trying to add a member + */ +function clearAddMemberError(reportID: string, accountID: number) { + const report = currentReportData?.[reportID]; + const participantAccountIDs = report?.participantAccountIDs?.filter((id: number) => id !== accountID); + const visibleChatMemberAccountIDs = report?.visibleChatMemberAccountIDs?.filter((id: number) => id !== accountID); + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { + pendingAccounts: { + [accountID]: null, + }, + participantAccountIDs, + visibleChatMemberAccountIDs, + }); +} + export { searchInServer, addComment, @@ -2758,4 +2785,6 @@ export { updateLastVisitTime, clearNewRoomFormError, resolveActionableMentionWhisper, + clearDeleteMemberError, + clearAddMemberError, }; diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js index 0ae0b74242ae..9e8674e2223c 100644 --- a/src/pages/RoomMembersPage.js +++ b/src/pages/RoomMembersPage.js @@ -206,7 +206,6 @@ function RoomMembersPage(props) { return; } } - result.push({ keyForList: String(accountID), accountID: Number(accountID), @@ -222,6 +221,7 @@ function RoomMembersPage(props) { }, ], pendingAction: _.get(pendingAccounts, [accountID, 'pendingAction']), + errors: _.get(pendingAccounts, [accountID, 'errors']), }); }); result = _.sortBy(result, (value) => value.text.toLowerCase()); @@ -229,6 +229,22 @@ function RoomMembersPage(props) { return result; }; + /** + * Dismisses the errors on one item + * + * @param {Object} item + */ + const dismissError = useCallback( + (item) => { + if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + Report.clearDeleteMemberError(props.report.reportID, item.accountID); + } else { + Report.clearAddMemberError(props.report.reportID, item.accountID); + } + }, + [props.report.reportID], + ); + const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(props.report.policyID, props.policies), [props.report.policyID, props.policies]); const data = getMemberOptions(); const headerMessage = searchValue.trim() && !data.length ? props.translate('roomMembersPage.memberNotFound') : ''; @@ -292,6 +308,7 @@ function RoomMembersPage(props) { showLoadingPlaceholder={!OptionsListUtils.isPersonalDetailsReady(personalDetails) || !didLoadRoomMembers} showScrollIndicator shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + onDismissError={dismissError} /> From 1f900683593ebee873dc38ec9eb66fdc31e1c254 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 30 Jan 2024 11:10:03 +0100 Subject: [PATCH 0133/1113] 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 2060d5e2bfc161a08b7058ce88b3603add93a3bc Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:15:47 +0530 Subject: [PATCH 0134/1113] TS-migration: TermsStep Page --- .../{TermsStep.js => TermsStep.tsx} | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) rename src/pages/EnablePayments/{TermsStep.js => TermsStep.tsx} (67%) diff --git a/src/pages/EnablePayments/TermsStep.js b/src/pages/EnablePayments/TermsStep.tsx similarity index 67% rename from src/pages/EnablePayments/TermsStep.js rename to src/pages/EnablePayments/TermsStep.tsx index a09e1801c3b0..a03f3607d56e 100644 --- a/src/pages/EnablePayments/TermsStep.js +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -6,39 +6,34 @@ import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; import LongTermsForm from './TermsPage/LongTermsForm'; import ShortTermsForm from './TermsPage/ShortTermsForm'; -import userWalletPropTypes from './userWalletPropTypes'; -import walletTermsPropTypes from './walletTermsPropTypes'; - -const propTypes = { - /** The user's wallet */ - userWallet: userWalletPropTypes, +import useLocalize from '@hooks/useLocalize'; +import type {OnyxEntry} from 'react-native-onyx'; +import {WalletTerms, UserWallet} from '@src/types/onyx'; +type TermsStepOnyxProps = { /** Comes from Onyx. Information about the terms for the wallet */ - walletTerms: walletTermsPropTypes, - - ...withLocalizePropTypes, -}; + walletTerms: OnyxEntry; +} -const defaultProps = { - userWallet: {}, - walletTerms: {}, +type TermsStepProps = TermsStepOnyxProps & { + /** The user's wallet */ + userWallet: OnyxEntry; }; -function TermsStep(props) { +function TermsStep(props: TermsStepProps) { const styles = useThemeStyles(); const [hasAcceptedDisclosure, setHasAcceptedDisclosure] = useState(false); const [hasAcceptedPrivacyPolicyAndWalletAgreement, setHasAcceptedPrivacyPolicyAndWalletAgreement] = useState(false); const [error, setError] = useState(false); + const {translate} = useLocalize(); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms) || ''; + const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms??{}) || ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); @@ -59,7 +54,7 @@ function TermsStep(props) { return ( <> - + ( - {`${props.translate('termsStep.haveReadAndAgree')}`} - {`${props.translate('termsStep.electronicDisclosures')}.`} + {`${translate('termsStep.haveReadAndAgree')}`} + {`${translate('termsStep.electronicDisclosures')}.`} )} /> ( - {`${props.translate('termsStep.agreeToThe')} `} + {`${translate('termsStep.agreeToThe')} `} - {`${props.translate('common.privacy')} `} + {`${translate('common.privacy')} `} - {`${props.translate('common.and')} `} + {`${translate('common.and')} `} - {`${props.translate('termsStep.walletAgreement')}.`} + {`${translate('termsStep.walletAgreement')}.`} )} /> { if (!hasAcceptedDisclosure || !hasAcceptedPrivacyPolicyAndWalletAgreement) { setError(true); @@ -104,12 +99,12 @@ function TermsStep(props) { setError(false); BankAccounts.acceptWalletTerms({ hasAcceptedTerms: hasAcceptedDisclosure && hasAcceptedPrivacyPolicyAndWalletAgreement, - reportID: props.walletTerms.chatReportID, + reportID: props.walletTerms?.chatReportID??'', }); }} message={errorMessage} isAlertVisible={error || Boolean(errorMessage)} - isLoading={!!props.walletTerms.isLoading} + isLoading={!!props.walletTerms?.isLoading} containerStyles={[styles.mh0, styles.mv4]} /> @@ -118,13 +113,9 @@ function TermsStep(props) { } TermsStep.displayName = 'TermsPage'; -TermsStep.propTypes = propTypes; -TermsStep.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - }), -)(TermsStep); + +export default withOnyx({ + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, +})(TermsStep); From 744a3b5f380317b65ac91925a7e1f37d36273a79 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:19:34 +0530 Subject: [PATCH 0135/1113] TS-migration: ShortTermsForm Page --- .../{ShortTermsForm.js => ShortTermsForm.tsx} | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) rename src/pages/EnablePayments/TermsPage/{ShortTermsForm.js => ShortTermsForm.tsx} (93%) diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx similarity index 93% rename from src/pages/EnablePayments/TermsPage/ShortTermsForm.js rename to src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx index 40824f47b036..1c9d37bb30e9 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx @@ -5,19 +5,16 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import CONST from '@src/CONST'; +import type {OnyxEntry} from 'react-native-onyx'; +import {UserWallet} from '@src/types/onyx'; -const propTypes = { - /** The user's wallet */ - userWallet: userWalletPropTypes, +type ShortTermsFormProps = { + /** The user's wallet */ + userWallet: OnyxEntry; }; -const defaultProps = { - userWallet: {}, -}; - -function ShortTermsForm(props) { +function ShortTermsForm(props: ShortTermsFormProps) { const styles = useThemeStyles(); const {translate, numberFormat} = useLocalize(); return ( @@ -25,7 +22,7 @@ function ShortTermsForm(props) { {translate('termsStep.shortTermsForm.expensifyPaymentsAccount', { walletProgram: - props.userWallet.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, + props.userWallet?.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, })} @@ -150,8 +147,6 @@ function ShortTermsForm(props) { ); } -ShortTermsForm.propTypes = propTypes; -ShortTermsForm.defaultProps = defaultProps; ShortTermsForm.displayName = 'ShortTermsForm'; export default ShortTermsForm; From 3f86b2eb3d06eef77cfc51c34a51b4945dddc1a3 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:22:23 +0530 Subject: [PATCH 0136/1113] TS-migration: LongTermsForm Page --- .../TermsPage/{LongTermsForm.js => LongTermsForm.tsx} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename src/pages/EnablePayments/TermsPage/{LongTermsForm.js => LongTermsForm.tsx} (98%) diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx similarity index 98% rename from src/pages/EnablePayments/TermsPage/LongTermsForm.js rename to src/pages/EnablePayments/TermsPage/LongTermsForm.tsx index fad19c5ecf6f..ec89856642d9 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx @@ -66,7 +66,7 @@ function LongTermsForm() { ]; const getLongTermsSections = () => - _.map(termsData, (section, index) => ( + termsData.map((section, index) => ( // eslint-disable-next-line react/no-array-index-key @@ -105,7 +105,6 @@ function LongTermsForm() { Date: Tue, 30 Jan 2024 20:24:10 +0530 Subject: [PATCH 0137/1113] TS-migration: FailedKYC Page --- .../{FailedKYC.js => FailedKYC.tsx} | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) rename src/pages/EnablePayments/{FailedKYC.js => FailedKYC.tsx} (65%) diff --git a/src/pages/EnablePayments/FailedKYC.js b/src/pages/EnablePayments/FailedKYC.tsx similarity index 65% rename from src/pages/EnablePayments/FailedKYC.js rename to src/pages/EnablePayments/FailedKYC.tsx index fc54ea9c1074..25672772c216 100644 --- a/src/pages/EnablePayments/FailedKYC.js +++ b/src/pages/EnablePayments/FailedKYC.tsx @@ -2,35 +2,31 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; +import useLocalize from '@hooks/useLocalize'; -const propTypes = { - ...withLocalizePropTypes, -}; - -function FailedKYC(props) { +function FailedKYC() { + const {translate} = useLocalize(); const styles = useThemeStyles(); return ( - {props.translate('additionalDetailsStep.failedKYCTextBefore')} + {translate('additionalDetailsStep.failedKYCTextBefore')} {CONST.EMAIL.CONCIERGE} - {props.translate('additionalDetailsStep.failedKYCTextAfter')} + {translate('additionalDetailsStep.failedKYCTextAfter')} ); } -FailedKYC.propTypes = propTypes; FailedKYC.displayName = 'FailedKYC'; -export default withLocalize(FailedKYC); +export default FailedKYC; From b4c5a8a9d66a629167114fd56c2bb68850a27d11 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:38:51 +0530 Subject: [PATCH 0138/1113] TS-migration: ActivateStep Page --- src/pages/EnablePayments/ActivateStep.js | 72 ----------------------- src/pages/EnablePayments/ActivateStep.tsx | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 72 deletions(-) delete mode 100644 src/pages/EnablePayments/ActivateStep.js create mode 100644 src/pages/EnablePayments/ActivateStep.tsx diff --git a/src/pages/EnablePayments/ActivateStep.js b/src/pages/EnablePayments/ActivateStep.js deleted file mode 100644 index 92342c28af73..000000000000 --- a/src/pages/EnablePayments/ActivateStep.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import ConfirmationPage from '@components/ConfirmationPage'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import LottieAnimations from '@components/LottieAnimations'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; -import * as PaymentMethods from '@userActions/PaymentMethods'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import userWalletPropTypes from './userWalletPropTypes'; -import walletTermsPropTypes from './walletTermsPropTypes'; - -const propTypes = { - ...withLocalizePropTypes, - - /** The user's wallet */ - userWallet: userWalletPropTypes, - - /** Information about the user accepting the terms for payments */ - walletTerms: walletTermsPropTypes, -}; - -const defaultProps = { - userWallet: {}, - walletTerms: { - source: '', - chatReportID: 0, - }, -}; - -function ActivateStep(props) { - const isActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], props.userWallet.tierName); - const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo; - let continueButtonText = ''; - - if (props.walletTerms.chatReportID) { - continueButtonText = props.translate('activateStep.continueToPayment'); - } else if (props.walletTerms.source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET) { - continueButtonText = props.translate('common.continue'); - } else { - continueButtonText = props.translate('activateStep.continueToTransfer'); - } - - return ( - <> - - PaymentMethods.continueSetup()} - /> - - ); -} - -ActivateStep.propTypes = propTypes; -ActivateStep.defaultProps = defaultProps; -ActivateStep.displayName = 'ActivateStep'; - -export default compose( - withLocalize, - withOnyx({ - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - }), -)(ActivateStep); diff --git a/src/pages/EnablePayments/ActivateStep.tsx b/src/pages/EnablePayments/ActivateStep.tsx new file mode 100644 index 000000000000..d16190f0c0af --- /dev/null +++ b/src/pages/EnablePayments/ActivateStep.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import ConfirmationPage from '@components/ConfirmationPage'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import LottieAnimations from '@components/LottieAnimations'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useLocalize from '@hooks/useLocalize'; +import type {OnyxEntry} from 'react-native-onyx'; +import {WalletTerms, UserWallet} from '@src/types/onyx'; + +type ActivateStepOnyxProps = { + /** Information about the user accepting the terms for payments */ + walletTerms: OnyxEntry; +}; + +type ActivateStepProps = ActivateStepOnyxProps & { + /** The user's wallet */ + userWallet: OnyxEntry; +}; + +function ActivateStep({ + userWallet, + walletTerms +}: ActivateStepProps) { + const {translate} = useLocalize(); + const isActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); + + const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo; + let continueButtonText = ''; + + if (walletTerms?.chatReportID) { + continueButtonText = translate('activateStep.continueToPayment'); + } else if (walletTerms?.source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET) { + continueButtonText = translate('common.continue'); + } else { + continueButtonText = translate('activateStep.continueToTransfer'); + } + + return ( + <> + + PaymentMethods.continueSetup()} + /> + + ); +} + +ActivateStep.displayName = 'ActivateStep'; + +export default withOnyx({ + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, +})(ActivateStep); \ No newline at end of file From e48ebb1d332c795947548c4027161b791b5869ff Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Tue, 30 Jan 2024 09:46:28 -0800 Subject: [PATCH 0139/1113] Display report description for 1-to-1 money request --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index edcd57078e0c..eeebe7e8b78a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2247,7 +2247,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } - const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); + const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; const lastActorID = reportAction?.actorAccountID; @@ -2259,14 +2259,14 @@ function getReportPreviewMessage( // We only want to show the actor name in the preview if it's not the current user who took the action const requestorName = lastActorID && lastActorID !== currentUserAccountID ? getDisplayNameForParticipant(lastActorID, !isPreviewMessageForParentChatReport) : ''; - return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay})}`; + return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay, comment})}`; } + const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); if (containsNonReimbursable) { return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } - const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } From 0ac0bedbd17cf7e86935e71828efd1175a7ac131 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 31 Jan 2024 01:59:28 +0530 Subject: [PATCH 0140/1113] TS-migration: OnfidoStep & OnfidoPrivacy Page --- src/pages/EnablePayments/ActivateStep.tsx | 13 ++-- src/pages/EnablePayments/FailedKYC.tsx | 2 +- .../{OnfidoPrivacy.js => OnfidoPrivacy.tsx} | 64 +++++++++---------- .../{OnfidoStep.js => OnfidoStep.tsx} | 35 +++++----- .../TermsPage/ShortTermsForm.tsx | 8 ++- src/pages/EnablePayments/TermsStep.tsx | 12 ++-- 6 files changed, 64 insertions(+), 70 deletions(-) rename src/pages/EnablePayments/{OnfidoPrivacy.js => OnfidoPrivacy.tsx} (74%) rename src/pages/EnablePayments/{OnfidoStep.js => OnfidoStep.tsx} (69%) diff --git a/src/pages/EnablePayments/ActivateStep.tsx b/src/pages/EnablePayments/ActivateStep.tsx index d16190f0c0af..0dbb98e53a5f 100644 --- a/src/pages/EnablePayments/ActivateStep.tsx +++ b/src/pages/EnablePayments/ActivateStep.tsx @@ -1,14 +1,14 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import LottieAnimations from '@components/LottieAnimations'; +import useLocalize from '@hooks/useLocalize'; import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import useLocalize from '@hooks/useLocalize'; -import type {OnyxEntry} from 'react-native-onyx'; -import {WalletTerms, UserWallet} from '@src/types/onyx'; +import {UserWallet, WalletTerms} from '@src/types/onyx'; type ActivateStepOnyxProps = { /** Information about the user accepting the terms for payments */ @@ -20,10 +20,7 @@ type ActivateStepProps = ActivateStepOnyxProps & { userWallet: OnyxEntry; }; -function ActivateStep({ - userWallet, - walletTerms -}: ActivateStepProps) { +function ActivateStep({userWallet, walletTerms}: ActivateStepProps) { const {translate} = useLocalize(); const isActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); @@ -59,4 +56,4 @@ export default withOnyx({ walletTerms: { key: ONYXKEYS.WALLET_TERMS, }, -})(ActivateStep); \ No newline at end of file +})(ActivateStep); diff --git a/src/pages/EnablePayments/FailedKYC.tsx b/src/pages/EnablePayments/FailedKYC.tsx index 25672772c216..6b393229d62f 100644 --- a/src/pages/EnablePayments/FailedKYC.tsx +++ b/src/pages/EnablePayments/FailedKYC.tsx @@ -2,9 +2,9 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import useLocalize from '@hooks/useLocalize'; function FailedKYC() { const {translate} = useLocalize(); diff --git a/src/pages/EnablePayments/OnfidoPrivacy.js b/src/pages/EnablePayments/OnfidoPrivacy.tsx similarity index 74% rename from src/pages/EnablePayments/OnfidoPrivacy.js rename to src/pages/EnablePayments/OnfidoPrivacy.tsx index 77b884fb2934..d8e9f616b8a7 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.js +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -1,7 +1,8 @@ import lodashGet from 'lodash/get'; import React, {useRef} from 'react'; -import {View} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import _ from 'underscore'; import FixedFooter from '@components/FixedFooter'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; @@ -9,43 +10,41 @@ import FormScrollView from '@components/FormScrollView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import walletOnfidoDataPropTypes from './walletOnfidoDataPropTypes'; +import {WalletOnfido} from '@src/types/onyx'; -const propTypes = { - /** Stores various information used to build the UI and call any APIs */ - walletOnfidoData: walletOnfidoDataPropTypes, - - ...withLocalizePropTypes, +const DEFAULT_WALLET_ONFIDO_DATA = { + applicantID: '', + sdkToken: '', + loading: false, + errors: {}, + fixableErrors: [], + hasAcceptedPrivacyPolicy: false, }; -const defaultProps = { - walletOnfidoData: { - applicantID: '', - sdkToken: '', - loading: false, - errors: {}, - fixableErrors: [], - hasAcceptedPrivacyPolicy: false, - }, +type OnfidoPrivacyOnyxProps = { + /** Stores various information used to build the UI and call any APIs */ + walletOnfidoData: OnyxEntry; }; -function OnfidoPrivacy({walletOnfidoData, translate, form}) { +type OnfidoPrivacyProps = OnfidoPrivacyOnyxProps & {}; + +function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPrivacyProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); - const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData; + const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData ?? {}; - const formRef = useRef(null); + const formRef = useRef(null); const openOnfidoFlow = () => { BankAccounts.openOnfidoFlow(); }; - let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; + let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) || ''; const onfidoFixableErrors = lodashGet(walletOnfidoData, 'fixableErrors', []); onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; @@ -70,7 +69,7 @@ function OnfidoPrivacy({walletOnfidoData, translate, form}) { isAlertVisible={Boolean(onfidoError)} onSubmit={openOnfidoFlow} onFixTheErrorsLinkPressed={() => { - form.scrollTo({y: 0, animated: true}); + formRef.current?.scrollTo({y: 0, animated: true}); }} message={onfidoError} isLoading={isLoading} @@ -85,18 +84,13 @@ function OnfidoPrivacy({walletOnfidoData, translate, form}) { ); } -OnfidoPrivacy.propTypes = propTypes; -OnfidoPrivacy.defaultProps = defaultProps; OnfidoPrivacy.displayName = 'OnfidoPrivacy'; -export default compose( - withLocalize, - withOnyx({ - walletOnfidoData: { - key: ONYXKEYS.WALLET_ONFIDO, +export default withOnyx({ + walletOnfidoData: { + key: ONYXKEYS.WALLET_ONFIDO, - // Let's get a new onfido token each time the user hits this flow (as it should only be once) - initWithStoredValues: false, - }, - }), -)(OnfidoPrivacy); + // Let's get a new onfido token each time the user hits this flow (as it should only be once) + initWithStoredValues: false, + }, +})(OnfidoPrivacy); diff --git a/src/pages/EnablePayments/OnfidoStep.js b/src/pages/EnablePayments/OnfidoStep.tsx similarity index 69% rename from src/pages/EnablePayments/OnfidoStep.js rename to src/pages/EnablePayments/OnfidoStep.tsx index 8b40c88f62fb..5e36f2f302a6 100644 --- a/src/pages/EnablePayments/OnfidoStep.js +++ b/src/pages/EnablePayments/OnfidoStep.tsx @@ -1,7 +1,9 @@ import React, {useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +// @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript. import Onfido from '@components/Onfido'; import useLocalize from '@hooks/useLocalize'; import Growl from '@libs/Growl'; @@ -11,25 +13,25 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {WalletOnfido} from '@src/types/onyx'; import OnfidoPrivacy from './OnfidoPrivacy'; -import walletOnfidoDataPropTypes from './walletOnfidoDataPropTypes'; -const propTypes = { - /** Stores various information used to build the UI and call any APIs */ - walletOnfidoData: walletOnfidoDataPropTypes, +const DEFAULT_WALLET_ONFIDO_DATA = { + loading: false, + hasAcceptedPrivacyPolicy: false, }; -const defaultProps = { - walletOnfidoData: { - loading: false, - hasAcceptedPrivacyPolicy: false, - }, +type OnfidoStepOnyxProps = { + /** Stores various information used to build the UI and call any APIs */ + walletOnfidoData: OnyxEntry; }; -function OnfidoStep({walletOnfidoData}) { +type OnfidoStepProps = OnfidoStepOnyxProps; + +function OnfidoStep({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoStepProps) { const {translate} = useLocalize(); - const shouldShowOnfido = walletOnfidoData.hasAcceptedPrivacyPolicy && !walletOnfidoData.isLoading && !walletOnfidoData.error && walletOnfidoData.sdkToken; + const shouldShowOnfido = walletOnfidoData?.hasAcceptedPrivacyPolicy && !walletOnfidoData.isLoading && !walletOnfidoData.errors && walletOnfidoData.sdkToken; const goBack = useCallback(() => { Navigation.goBack(ROUTES.HOME); @@ -44,15 +46,16 @@ function OnfidoStep({walletOnfidoData}) { }, [translate]); const verifyIdentity = useCallback( + // @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript. (data) => { BankAccounts.verifyIdentity({ onfidoData: JSON.stringify({ ...data, - applicantID: walletOnfidoData.applicantID, + applicantID: walletOnfidoData?.applicantID, }), }); }, - [walletOnfidoData.applicantID], + [walletOnfidoData?.applicantID], ); return ( @@ -70,18 +73,16 @@ function OnfidoStep({walletOnfidoData}) { onSuccess={verifyIdentity} /> ) : ( - + )} ); } -OnfidoStep.propTypes = propTypes; -OnfidoStep.defaultProps = defaultProps; OnfidoStep.displayName = 'OnfidoStep'; -export default withOnyx({ +export default withOnyx({ walletOnfidoData: { key: ONYXKEYS.WALLET_ONFIDO, diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx index 1c9d37bb30e9..b45b8b657a75 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx @@ -1,16 +1,16 @@ import React from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; -import type {OnyxEntry} from 'react-native-onyx'; import {UserWallet} from '@src/types/onyx'; type ShortTermsFormProps = { - /** The user's wallet */ + /** The user's wallet */ userWallet: OnyxEntry; }; @@ -22,7 +22,9 @@ function ShortTermsForm(props: ShortTermsFormProps) { {translate('termsStep.shortTermsForm.expensifyPaymentsAccount', { walletProgram: - props.userWallet?.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, + props.userWallet?.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID + ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS + : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, })} diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index a03f3607d56e..cef54ba463e6 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -1,25 +1,25 @@ import React, {useEffect, useState} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; +import {UserWallet, WalletTerms} from '@src/types/onyx'; import LongTermsForm from './TermsPage/LongTermsForm'; import ShortTermsForm from './TermsPage/ShortTermsForm'; -import useLocalize from '@hooks/useLocalize'; -import type {OnyxEntry} from 'react-native-onyx'; -import {WalletTerms, UserWallet} from '@src/types/onyx'; type TermsStepOnyxProps = { /** Comes from Onyx. Information about the terms for the wallet */ walletTerms: OnyxEntry; -} +}; type TermsStepProps = TermsStepOnyxProps & { /** The user's wallet */ @@ -33,7 +33,7 @@ function TermsStep(props: TermsStepProps) { const [error, setError] = useState(false); const {translate} = useLocalize(); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms??{}) || ''; + const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) || ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); @@ -99,7 +99,7 @@ function TermsStep(props: TermsStepProps) { setError(false); BankAccounts.acceptWalletTerms({ hasAcceptedTerms: hasAcceptedDisclosure && hasAcceptedPrivacyPolicyAndWalletAgreement, - reportID: props.walletTerms?.chatReportID??'', + reportID: props.walletTerms?.chatReportID ?? '', }); }} message={errorMessage} From 0feb8c4e71dad1c3cd3d11f7a2c7670387bd09c7 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 31 Jan 2024 04:23:28 +0530 Subject: [PATCH 0141/1113] TS-migration: EnablePaymentsPage Page --- ...PaymentsPage.js => EnablePaymentsPage.tsx} | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) rename src/pages/EnablePayments/{EnablePaymentsPage.js => EnablePaymentsPage.tsx} (78%) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.tsx similarity index 78% rename from src/pages/EnablePayments/EnablePaymentsPage.js rename to src/pages/EnablePayments/EnablePaymentsPage.tsx index 257eab1d38d3..6d2defc82df0 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.tsx @@ -1,6 +1,6 @@ import React, {useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -11,28 +11,27 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {UserWallet} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ActivateStep from './ActivateStep'; import AdditionalDetailsStep from './AdditionalDetailsStep'; import FailedKYC from './FailedKYC'; // Steps import OnfidoStep from './OnfidoStep'; import TermsStep from './TermsStep'; -import userWalletPropTypes from './userWalletPropTypes'; -const propTypes = { +type EnablePaymentsPageOnyxProps = { /** The user's wallet */ - userWallet: userWalletPropTypes, + userWallet: OnyxEntry; }; -const defaultProps = { - userWallet: {}, -}; +type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps & {}; -function EnablePaymentsPage({userWallet}) { +function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {isPendingOnfidoResult, hasFailedOnfido} = userWallet; + const {isPendingOnfidoResult, hasFailedOnfido} = userWallet ?? {}; useEffect(() => { if (isOffline) { @@ -47,18 +46,18 @@ function EnablePaymentsPage({userWallet}) { Wallet.openEnablePaymentsPage(); }, [isOffline, isPendingOnfidoResult, hasFailedOnfido]); - if (_.isEmpty(userWallet)) { + if (isEmptyObject(userWallet)) { return ; } return ( {() => { - if (userWallet.errorCode === CONST.WALLET.ERROR.KYC) { + if (userWallet?.errorCode === CONST.WALLET.ERROR.KYC) { return ( <> ({ userWallet: { key: ONYXKEYS.USER_WALLET, From 44c41df41b81ab483fe0c3a13505b01fab0084fa Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 31 Jan 2024 15:34:26 +0100 Subject: [PATCH 0142/1113] back changes --- src/components/PDFView/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index 5d505bd318fe..4e748669df1b 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -4,7 +4,9 @@ import {PDFPreviewer} from 'react-fast-pdf'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import Text from '@components/Text'; import withLocalize from '@components/withLocalize'; import withThemeStyles from '@components/withThemeStyles'; import withWindowDimensions from '@components/withWindowDimensions'; @@ -93,6 +95,7 @@ class PDFView extends Component { maxCanvasWidth={this.props.maxCanvasWidth} maxCanvasHeight={this.props.maxCanvasHeight} maxCanvasArea={this.props.maxCanvasArea} + LoadingComponent={} ErrorComponent={{this.props.translate('attachmentView.failedToLoadPDF')}} renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => ( Date: Wed, 31 Jan 2024 18:29:12 +0100 Subject: [PATCH 0143/1113] use latest react-fast-pdf versions --- package-lock.json | 34 +++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04409926e0b1..b829a7208f72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "react-content-loader": "^6.1.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#b7b8ccb6a21a2322d0567bd448d57e54d71238fb", + "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#b925199ddbc94eb17197bfbe285e764d95832206", "react-map-gl": "^7.1.3", "react-native": "0.73.2", "react-native-android-location-enabler": "^1.2.2", @@ -28041,7 +28041,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -44622,12 +44621,12 @@ }, "node_modules/react-fast-pdf": { "version": "1.0.3", - "resolved": "git+ssh://git@github.com/rezkiy37/react-fast-pdf.git#b7b8ccb6a21a2322d0567bd448d57e54d71238fb", - "integrity": "sha512-qTePMIJJX1fKe/o83pu/zHYkGforwT2HnHK3pRBaZ1xe5CD3xCM5iQ6K0xRNTxQ/F4saLD0oQtiIjaKZdmdIBg==", + "resolved": "git+ssh://git@github.com/rezkiy37/react-fast-pdf.git#b925199ddbc94eb17197bfbe285e764d95832206", + "integrity": "sha512-CRPbAXZkIYc9e3Dk93qzGD63jfqAvzg57ay2S36Do1V1UwKUFTkwwM+kbbC+kwejVq05tnIC5CRthU75PnlP1A==", "license": "MIT", "dependencies": { - "react-pdf": "^7.3.3", - "react-window": "^1.8.9" + "react-pdf": "^7.7.0", + "react-window": "^1.8.10" }, "engines": { "node": "20.10.0", @@ -44653,18 +44652,19 @@ } }, "node_modules/react-fast-pdf/node_modules/react-pdf": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.6.0.tgz", - "integrity": "sha512-b2/8V6xhe5pn4Y01ELKTQZ+RhdZl1KpSAMGbN+HCZ/kwhYTIc4Pn5ctz1wRQUu1gOJbIEG4CcjMD9vTCzNdwjw==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.0.tgz", + "integrity": "sha512-704ObLnRDm5lixL4e6NXNLaincBHGNLo+NGdbO3rEXE963NlNzwLxFpmKcbdXHAMQL4rYJQWb1L0w5IL6y8Osw==", "dependencies": { "clsx": "^2.0.0", + "dequal": "^2.0.3", "make-cancellable-promise": "^1.3.1", "make-event-props": "^1.6.0", "merge-refs": "^1.2.1", "pdfjs-dist": "3.11.174", "prop-types": "^15.6.2", "tiny-invariant": "^1.0.0", - "tiny-warning": "^1.0.0" + "warning": "^4.0.0" }, "funding": { "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" @@ -44680,6 +44680,14 @@ } } }, + "node_modules/react-fast-pdf/node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/react-freeze": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz", @@ -46301,9 +46309,9 @@ } }, "node_modules/react-window": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.9.tgz", - "integrity": "sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==", + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" diff --git a/package.json b/package.json index 56d74250d929..2ce9f9bbe492 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "react-content-loader": "^6.1.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#b7b8ccb6a21a2322d0567bd448d57e54d71238fb", + "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#b925199ddbc94eb17197bfbe285e764d95832206", "react-map-gl": "^7.1.3", "react-native": "0.73.2", "react-native-android-location-enabler": "^1.2.2", From 55a4a1115a0124c585b6f991f5e59371c245206b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 31 Jan 2024 18:37:56 +0100 Subject: [PATCH 0144/1113] use latest react-fast-pdf versions --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b829a7208f72..df4c333af633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "react-content-loader": "^6.1.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#b925199ddbc94eb17197bfbe285e764d95832206", + "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#d5627b27a2d73048c2d12aa2044e44332f92bcf3", "react-map-gl": "^7.1.3", "react-native": "0.73.2", "react-native-android-location-enabler": "^1.2.2", @@ -44621,8 +44621,8 @@ }, "node_modules/react-fast-pdf": { "version": "1.0.3", - "resolved": "git+ssh://git@github.com/rezkiy37/react-fast-pdf.git#b925199ddbc94eb17197bfbe285e764d95832206", - "integrity": "sha512-CRPbAXZkIYc9e3Dk93qzGD63jfqAvzg57ay2S36Do1V1UwKUFTkwwM+kbbC+kwejVq05tnIC5CRthU75PnlP1A==", + "resolved": "git+ssh://git@github.com/rezkiy37/react-fast-pdf.git#d5627b27a2d73048c2d12aa2044e44332f92bcf3", + "integrity": "sha512-YeEIjzBpxEmELZqQg6Ve3qJngr81fYA+b1or4EHR0dnx6/oGElEipAB17e+ZuCJ/NnPc3xKbUEY8UpqA/7+ssw==", "license": "MIT", "dependencies": { "react-pdf": "^7.7.0", diff --git a/package.json b/package.json index 2ce9f9bbe492..b546405d2917 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "react-content-loader": "^6.1.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#b925199ddbc94eb17197bfbe285e764d95832206", + "react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf#d5627b27a2d73048c2d12aa2044e44332f92bcf3", "react-map-gl": "^7.1.3", "react-native": "0.73.2", "react-native-android-location-enabler": "^1.2.2", From 4feb09259b4501462807da3bb197aa705e3c1d4f Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Tue, 23 Jan 2024 13:44:07 +0000 Subject: [PATCH 0145/1113] [TS Migration] Migrate WorkspaceCard to Typescript --- .../workspace/card/WorkspaceCardNoVBAView.js | 49 ------------------- .../workspace/card/WorkspaceCardNoVBAView.tsx | 40 +++++++++++++++ src/pages/workspace/card/WorkspaceCardPage.js | 47 ------------------ .../workspace/card/WorkspaceCardPage.tsx | 39 +++++++++++++++ ...iew.js => WorkspaceCardVBANoECardView.tsx} | 48 +++++++----------- ...w.js => WorkspaceCardVBAWithECardView.tsx} | 39 ++++++++------- 6 files changed, 117 insertions(+), 145 deletions(-) delete mode 100644 src/pages/workspace/card/WorkspaceCardNoVBAView.js create mode 100644 src/pages/workspace/card/WorkspaceCardNoVBAView.tsx delete mode 100644 src/pages/workspace/card/WorkspaceCardPage.js create mode 100644 src/pages/workspace/card/WorkspaceCardPage.tsx rename src/pages/workspace/card/{WorkspaceCardVBANoECardView.js => WorkspaceCardVBANoECardView.tsx} (53%) rename src/pages/workspace/card/{WorkspaceCardVBAWithECardView.js => WorkspaceCardVBAWithECardView.tsx} (67%) diff --git a/src/pages/workspace/card/WorkspaceCardNoVBAView.js b/src/pages/workspace/card/WorkspaceCardNoVBAView.js deleted file mode 100644 index 3233f8ea7e23..000000000000 --- a/src/pages/workspace/card/WorkspaceCardNoVBAView.js +++ /dev/null @@ -1,49 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import ConnectBankAccountButton from '@components/ConnectBankAccountButton'; -import * as Illustrations from '@components/Icon/Illustrations'; -import Section from '@components/Section'; -import Text from '@components/Text'; -import UnorderedList from '@components/UnorderedList'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; - -const propTypes = { - /** The policy ID currently being configured */ - policyID: PropTypes.string.isRequired, - - ...withLocalizePropTypes, -}; - -function WorkspaceCardNoVBAView(props) { - const styles = useThemeStyles(); - return ( -
- - {props.translate('workspace.card.noVBACopy')} - - - - -
- ); -} - -WorkspaceCardNoVBAView.propTypes = propTypes; -WorkspaceCardNoVBAView.displayName = 'WorkspaceCardNoVBAView'; - -export default withLocalize(WorkspaceCardNoVBAView); diff --git a/src/pages/workspace/card/WorkspaceCardNoVBAView.tsx b/src/pages/workspace/card/WorkspaceCardNoVBAView.tsx new file mode 100644 index 000000000000..322d433a8e62 --- /dev/null +++ b/src/pages/workspace/card/WorkspaceCardNoVBAView.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import {View} from 'react-native'; +import ConnectBankAccountButton from '@components/ConnectBankAccountButton'; +import * as Illustrations from '@components/Icon/Illustrations'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import UnorderedList from '@components/UnorderedList'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type WorkspaceCardNoVBAViewProps = { + /** The policy ID currently being configured */ + policyID: string; +}; + +function WorkspaceCardNoVBAView({policyID}: WorkspaceCardNoVBAViewProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( +
+ + {translate('workspace.card.noVBACopy')} + + + + +
+ ); +} + +WorkspaceCardNoVBAView.displayName = 'WorkspaceCardNoVBAView'; + +export default WorkspaceCardNoVBAView; diff --git a/src/pages/workspace/card/WorkspaceCardPage.js b/src/pages/workspace/card/WorkspaceCardPage.js deleted file mode 100644 index 55220b85ce63..000000000000 --- a/src/pages/workspace/card/WorkspaceCardPage.js +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import CONST from '@src/CONST'; -import WorkspaceCardNoVBAView from './WorkspaceCardNoVBAView'; -import WorkspaceCardVBANoECardView from './WorkspaceCardVBANoECardView'; -import WorkspaceCardVBAWithECardView from './WorkspaceCardVBAWithECardView'; - -const propTypes = { - /** The route object passed to this page from the navigator */ - route: PropTypes.shape({ - /** Each parameter passed via the URL */ - params: PropTypes.shape({ - /** The policyID that is being configured */ - policyID: PropTypes.string.isRequired, - }).isRequired, - }).isRequired, - - ...withLocalizePropTypes, -}; - -function WorkspaceCardPage(props) { - return ( - - {(hasVBA, policyID, isUsingECard) => ( - <> - {!hasVBA && } - - {hasVBA && !isUsingECard && } - - {hasVBA && isUsingECard && } - - )} - - ); -} - -WorkspaceCardPage.propTypes = propTypes; -WorkspaceCardPage.displayName = 'WorkspaceCardPage'; - -export default withLocalize(WorkspaceCardPage); diff --git a/src/pages/workspace/card/WorkspaceCardPage.tsx b/src/pages/workspace/card/WorkspaceCardPage.tsx new file mode 100644 index 000000000000..f6e368db84ea --- /dev/null +++ b/src/pages/workspace/card/WorkspaceCardPage.tsx @@ -0,0 +1,39 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; +import WorkspaceCardNoVBAView from './WorkspaceCardNoVBAView'; +import WorkspaceCardVBANoECardView from './WorkspaceCardVBANoECardView'; +import WorkspaceCardVBAWithECardView from './WorkspaceCardVBAWithECardView'; + +type WorkspaceCardPageProps = StackScreenProps; + +function WorkspaceCardPage({route}: WorkspaceCardPageProps) { + const {translate} = useLocalize(); + + return ( + + {(hasVBA: boolean, policyID: string, isUsingECard: boolean) => ( + <> + {false && } + + {false && } + + {true && } + + )} + + ); +} + +WorkspaceCardPage.displayName = 'WorkspaceCardPage'; + +export default WorkspaceCardPage; diff --git a/src/pages/workspace/card/WorkspaceCardVBANoECardView.js b/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx similarity index 53% rename from src/pages/workspace/card/WorkspaceCardVBANoECardView.js rename to src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx index 970cd9105368..3c9b773d6994 100644 --- a/src/pages/workspace/card/WorkspaceCardVBANoECardView.js +++ b/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -7,45 +8,37 @@ import * as Illustrations from '@components/Icon/Illustrations'; import Section from '@components/Section'; import Text from '@components/Text'; import UnorderedList from '@components/UnorderedList'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import userPropTypes from '@pages/settings/userPropTypes'; import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {User} from '@src/types/onyx'; -const propTypes = { +type WorkspaceCardVBANoECardViewOnyxProps = { /** Information about the logged in user's account */ - user: userPropTypes, - - ...withLocalizePropTypes, + user: OnyxEntry; }; -const defaultProps = { - user: {}, -}; +type WorkspaceCardVBANoECardViewProps = WorkspaceCardVBANoECardViewOnyxProps; -function WorkspaceCardVBANoECardView(props) { +function WorkspaceCardVBANoECardView({user}: WorkspaceCardVBANoECardViewProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); + return ( <>
- {Boolean(props.user.isCheckingDomain) && {props.translate('workspace.card.checkingDomain')}} + {Boolean(user?.isCheckingDomain) && {translate('workspace.card.checkingDomain')}} ); } -WorkspaceCardVBANoECardView.propTypes = propTypes; -WorkspaceCardVBANoECardView.defaultProps = defaultProps; WorkspaceCardVBANoECardView.displayName = 'WorkspaceCardVBANoECardView'; -export default compose( - withLocalize, - withOnyx({ - user: { - key: ONYXKEYS.USER, - }, - }), -)(WorkspaceCardVBANoECardView); +export default withOnyx({ + user: { + key: ONYXKEYS.USER, + }, +})(WorkspaceCardVBANoECardView); diff --git a/src/pages/workspace/card/WorkspaceCardVBAWithECardView.js b/src/pages/workspace/card/WorkspaceCardVBAWithECardView.tsx similarity index 67% rename from src/pages/workspace/card/WorkspaceCardVBAWithECardView.js rename to src/pages/workspace/card/WorkspaceCardVBAWithECardView.tsx index 40ecd80b8e6e..a53a44fa52cf 100644 --- a/src/pages/workspace/card/WorkspaceCardVBAWithECardView.js +++ b/src/pages/workspace/card/WorkspaceCardVBAWithECardView.tsx @@ -2,28 +2,35 @@ import React from 'react'; import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import type {MenuItemWithLink} from '@components/MenuItemList'; import Section from '@components/Section'; import Text from '@components/Text'; import UnorderedList from '@components/UnorderedList'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Link from '@userActions/Link'; -const propTypes = { - ...withLocalizePropTypes, +type MenuLinks = { + ISSUE_AND_MANAGE_CARDS: string; + RECONCILE_CARDS: string; + SETTLEMENT_FREQUENCY: string; }; -const MENU_LINKS = { +type MenuItems = MenuItemWithLink[]; + +const MENU_LINKS: MenuLinks = { ISSUE_AND_MANAGE_CARDS: 'domain_companycards', RECONCILE_CARDS: encodeURI('domain_companycards?param={"section":"cardReconciliation"}'), SETTLEMENT_FREQUENCY: encodeURI('domain_companycards?param={"section":"configureSettings"}'), }; -function WorkspaceCardVBAWithECardView(props) { +function WorkspaceCardVBAWithECardView() { const styles = useThemeStyles(); - const menuItems = [ + const {translate} = useLocalize(); + + const menuItems: MenuItems = [ { - title: props.translate('workspace.common.issueAndManageCards'), + title: translate('workspace.common.issueAndManageCards'), onPress: () => Link.openOldDotLink(MENU_LINKS.ISSUE_AND_MANAGE_CARDS), icon: Expensicons.ExpensifyCard, shouldShowRightIcon: true, @@ -32,7 +39,7 @@ function WorkspaceCardVBAWithECardView(props) { link: () => Link.buildOldDotURL(MENU_LINKS.ISSUE_AND_MANAGE_CARDS), }, { - title: props.translate('workspace.common.reconcileCards'), + title: translate('workspace.common.reconcileCards'), onPress: () => Link.openOldDotLink(MENU_LINKS.RECONCILE_CARDS), icon: Expensicons.ReceiptSearch, shouldShowRightIcon: true, @@ -41,7 +48,7 @@ function WorkspaceCardVBAWithECardView(props) { link: () => Link.buildOldDotURL(MENU_LINKS.RECONCILE_CARDS), }, { - title: props.translate('workspace.common.settlementFrequency'), + title: translate('workspace.common.settlementFrequency'), onPress: () => Link.openOldDotLink(MENU_LINKS.SETTLEMENT_FREQUENCY), icon: Expensicons.Gear, shouldShowRightIcon: true, @@ -53,29 +60,23 @@ function WorkspaceCardVBAWithECardView(props) { return (
- {props.translate('workspace.card.VBAWithECardCopy')} + {translate('workspace.card.VBAWithECardCopy')}
); } -WorkspaceCardVBAWithECardView.propTypes = propTypes; WorkspaceCardVBAWithECardView.displayName = 'WorkspaceCardVBAWithECardView'; -export default withLocalize(WorkspaceCardVBAWithECardView); +export default WorkspaceCardVBAWithECardView; From b9746297385965d1a1ba4150ab10e4110aef012d Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 1 Feb 2024 10:15:16 +0000 Subject: [PATCH 0146/1113] [TS migration] Migrate WorkspaceReimburse page --- src/ONYXKEYS.ts | 4 +- src/components/Picker/BasePicker.tsx | 4 +- src/components/Picker/types.ts | 2 +- src/libs/DistanceRequestUtils.ts | 2 +- src/libs/PolicyUtils.ts | 4 +- src/libs/actions/Policy.ts | 17 +- .../reimburse/WorkspaceRateAndUnitPage.js | 173 ------------------ .../reimburse/WorkspaceRateAndUnitPage.tsx | 157 ++++++++++++++++ .../reimburse/WorkspaceReimbursePage.js | 42 ----- .../reimburse/WorkspaceReimbursePage.tsx | 33 ++++ ...ction.js => WorkspaceReimburseSection.tsx} | 60 +++--- ...urseView.js => WorkspaceReimburseView.tsx} | 116 ++++-------- src/types/onyx/Form.ts | 9 +- src/types/onyx/Policy.ts | 6 +- src/types/onyx/index.ts | 3 +- 15 files changed, 291 insertions(+), 341 deletions(-) delete mode 100644 src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js create mode 100644 src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx delete mode 100644 src/pages/workspace/reimburse/WorkspaceReimbursePage.js create mode 100644 src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx rename src/pages/workspace/reimburse/{WorkspaceReimburseSection.js => WorkspaceReimburseSection.tsx} (57%) rename src/pages/workspace/reimburse/{WorkspaceReimburseView.js => WorkspaceReimburseView.tsx} (53%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2867cb3905a2..0eaa79d7cdda 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -486,8 +486,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.RateUnitForm; + [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM_DRAFT]: OnyxTypes.RateUnitForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 1bee95532104..020a3cf72680 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -69,11 +69,11 @@ function BasePicker( */ const onValueChange = (inputValue: TPickerValue, index: number) => { if (inputID) { - onInputChange(inputValue); + onInputChange?.(inputValue); return; } - onInputChange(inputValue, index); + onInputChange?.(inputValue, index); }; const enableHighlight = () => { diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index edf39a59c9d8..6304b23e7a2c 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -73,7 +73,7 @@ type BasePickerProps = { shouldSaveDraft?: boolean; /** A callback method that is called when the value changes and it receives the selected value as an argument */ - onInputChange: (value: TPickerValue, index?: number) => void; + onInputChange?: (value: TPickerValue, index?: number) => void; /** Size of a picker component */ size?: PickerSize; diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c92e9bfd3f67..fb4e92d7f147 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -100,7 +100,7 @@ function getDistanceMerchant( const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; - const ratePerUnit = rate ? PolicyUtils.getUnitRateValue({rate}, toLocaleDigit) : translate('common.tbd'); + const ratePerUnit = rate ? PolicyUtils.getUnitRateValue(toLocaleDigit, {rate}) : translate('common.tbd'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const currencySymbol = rate ? CurrencyUtils.getCurrencySymbol(currency) || `${currency} ` : ''; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b8ed62f93082..a812cab24402 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -4,11 +4,11 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; +import type {Rate} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type MemberEmailsToAccountIDs = Record; -type UnitRate = {rate: number}; /** * Filter out the active policies, which will exclude policies with pending deletion @@ -66,7 +66,7 @@ function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => stri return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.toString().length); } -function getUnitRateValue(customUnitRate: UnitRate, toLocaleDigit: (arg: string) => string) { +function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate?: Rate) { return getRateDisplayValue((customUnitRate?.rate ?? 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit); } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index fbe92aeb378d..48fd8944f196 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -67,6 +67,13 @@ type OptimisticCustomUnits = { outputCurrency: string; }; +type NewCustomUnit = { + customUnitID: string; + name: string; + attributes: Attributes; + rates: Rate; +}; + type PoliciesRecord = Record>; const allPolicies: OnyxCollection = {}; @@ -932,7 +939,7 @@ function hideWorkspaceAlertMessage(policyID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {alertMessage: ''}); } -function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: CustomUnit, lastModified: number) { +function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: NewCustomUnit, lastModified: number) { if (!currentCustomUnit.customUnitID || !newCustomUnit?.customUnitID || !newCustomUnit.rates?.customUnitRateID) { return; } @@ -946,7 +953,7 @@ function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: C [newCustomUnit.customUnitID]: { ...newCustomUnit, rates: { - [newCustomUnit.rates.customUnitRateID as string]: { + [newCustomUnit.rates.customUnitRateID]: { ...newCustomUnit.rates, errors: null, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -969,7 +976,7 @@ function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: C pendingAction: null, errors: null, rates: { - [newCustomUnit.rates.customUnitRateID as string]: { + [newCustomUnit.rates.customUnitRateID]: { pendingAction: null, }, }, @@ -988,7 +995,7 @@ function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: C [currentCustomUnit.customUnitID]: { customUnitID: currentCustomUnit.customUnitID, rates: { - [newCustomUnit.rates.customUnitRateID as string]: { + [newCustomUnit.rates.customUnitRateID]: { ...currentCustomUnit.rates, errors: ErrorUtils.getMicroSecondOnyxError('workspace.reimburse.updateCustomUnitError'), }, @@ -2018,3 +2025,5 @@ export { createDraftInitialWorkspace, setWorkspaceInviteMessageDraft, }; + +export type {NewCustomUnit}; diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js deleted file mode 100644 index 93ea7212e741..000000000000 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js +++ /dev/null @@ -1,173 +0,0 @@ -import lodashGet from 'lodash/get'; -import React, {useEffect} from 'react'; -import {Keyboard, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; -import Picker from '@components/Picker'; -import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import compose from '@libs/compose'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import getPermittedDecimalSeparator from '@libs/getPermittedDecimalSeparator'; -import Navigation from '@libs/Navigation/Navigation'; -import * as NumberUtils from '@libs/NumberUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import withPolicy, {policyDefaultProps, policyPropTypes} from '@pages/workspace/withPolicy'; -import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import * as BankAccounts from '@userActions/BankAccounts'; -import * as Policy from '@userActions/Policy'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -const propTypes = { - ...policyPropTypes, - ...withLocalizePropTypes, - ...withThemeStylesPropTypes, -}; - -const defaultProps = { - reimbursementAccount: {}, - ...policyDefaultProps, -}; - -function WorkspaceRateAndUnitPage(props) { - useEffect(() => { - if (lodashGet(props, 'policy.customUnits', []).length !== 0) { - return; - } - - BankAccounts.setReimbursementAccountLoading(true); - Policy.openWorkspaceReimburseView(props.policy.id); - }, [props]); - - const unitItems = [ - {label: props.translate('common.kilometers'), value: CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS}, - {label: props.translate('common.miles'), value: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, - ]; - - const saveUnitAndRate = (unit, rate) => { - const distanceCustomUnit = _.find(lodashGet(props, 'policy.customUnits', {}), (customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); - if (!distanceCustomUnit) { - return; - } - const currentCustomUnitRate = _.find(lodashGet(distanceCustomUnit, 'rates', {}), (r) => r.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); - const unitID = lodashGet(distanceCustomUnit, 'customUnitID', ''); - const unitName = lodashGet(distanceCustomUnit, 'name', ''); - const rateNumValue = PolicyUtils.getNumericValue(rate, props.toLocaleDigit); - - const newCustomUnit = { - customUnitID: unitID, - name: unitName, - attributes: {unit}, - rates: { - ...currentCustomUnitRate, - rate: rateNumValue * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, - }, - }; - Policy.updateWorkspaceCustomUnitAndRate(props.policy.id, distanceCustomUnit, newCustomUnit, props.policy.lastModified); - }; - - const submit = (values) => { - saveUnitAndRate(values.unit, values.rate); - Keyboard.dismiss(); - Navigation.goBack(ROUTES.WORKSPACE_REIMBURSE.getRoute(props.policy.id)); - }; - - const validate = (values) => { - const errors = {}; - const decimalSeparator = props.toLocaleDigit('.'); - const outputCurrency = lodashGet(props, 'policy.outputCurrency', CONST.CURRENCY.USD); - // Allow one more decimal place for accuracy - const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,${CurrencyUtils.getCurrencyDecimals(outputCurrency) + 1}})?$`, 'i'); - if (!rateValueRegex.test(values.rate) || values.rate === '') { - errors.rate = 'workspace.reimburse.invalidRateError'; - } else if (NumberUtils.parseFloatAnyLocale(values.rate) <= 0) { - errors.rate = 'workspace.reimburse.lowRateError'; - } - return errors; - }; - - const distanceCustomUnit = _.find(lodashGet(props, 'policy.customUnits', {}), (unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); - const distanceCustomRate = _.find(lodashGet(distanceCustomUnit, 'rates', {}), (rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); - - return ( - - {() => ( - - - Policy.clearCustomUnitErrors(props.policy.id, lodashGet(distanceCustomUnit, 'customUnitID', ''), lodashGet(distanceCustomRate, 'customUnitRateID', '')) - } - > - - - - - - - - )} - - ); -} - -WorkspaceRateAndUnitPage.propTypes = propTypes; -WorkspaceRateAndUnitPage.defaultProps = defaultProps; -WorkspaceRateAndUnitPage.displayName = 'WorkspaceRateAndUnitPage'; - -export default compose( - withPolicy, - withLocalize, - withNetwork(), - withOnyx({ - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - }, - }), - withThemeStyles, -)(WorkspaceRateAndUnitPage); diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx new file mode 100644 index 000000000000..9c24b6bde023 --- /dev/null +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx @@ -0,0 +1,157 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect} from 'react'; +import {Keyboard, View} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import Picker from '@components/Picker'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import getPermittedDecimalSeparator from '@libs/getPermittedDecimalSeparator'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as NumberUtils from '@libs/NumberUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import withPolicy from '@pages/workspace/withPolicy'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import * as BankAccounts from '@userActions/BankAccounts'; +import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Unit} from '@src/types/onyx/Policy'; + +type WorkspaceRateAndUnitPageProps = WithPolicyProps & StackScreenProps; + +type ValidationError = {rate?: string}; + +function WorkspaceRateAndUnitPage({policy, route}: WorkspaceRateAndUnitPageProps) { + const {translate, toLocaleDigit} = useLocalize(); + const styles = useThemeStyles(); + + useEffect(() => { + if ((policy?.customUnits ?? []).length !== 0) { + return; + } + + BankAccounts.setReimbursementAccountLoading(true); + Policy.openWorkspaceReimburseView(policy?.id ?? ''); + }, [policy?.customUnits, policy?.id]); + + const unitItems = [ + {label: translate('common.kilometers'), value: CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS}, + {label: translate('common.miles'), value: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, + ]; + + const saveUnitAndRate = (unit: Unit, rate: number) => { + const distanceCustomUnit = Object.values(policy?.customUnits ?? {}).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + if (!distanceCustomUnit) { + return; + } + const currentCustomUnitRate = Object.values(distanceCustomUnit?.rates ?? {}).find((r) => r.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); + const unitID = distanceCustomUnit.customUnitID ?? ''; + const unitName = distanceCustomUnit.name ?? ''; + const rateNumValue = PolicyUtils.getNumericValue(rate, toLocaleDigit) as number; + + const newCustomUnit: Policy.NewCustomUnit = { + customUnitID: unitID, + name: unitName, + attributes: {unit}, + rates: { + ...currentCustomUnitRate, + rate: rateNumValue * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, + }, + }; + + Policy.updateWorkspaceCustomUnitAndRate(policy?.id ?? '', distanceCustomUnit, newCustomUnit, parseInt(policy?.lastModified ?? '', 2)); + }; + + const submit = (values: OnyxFormValuesFields) => { + saveUnitAndRate(values.unit, values.rate); + Keyboard.dismiss(); + Navigation.goBack(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy?.id ?? '')); + }; + + const validate = (values: OnyxFormValuesFields): ValidationError => { + const errors: ValidationError = {}; + const decimalSeparator = toLocaleDigit('.'); + const outputCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; + // Allow one more decimal place for accuracy + const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,${CurrencyUtils.getCurrencyDecimals(outputCurrency) + 1}})?$`, 'i'); + if (!rateValueRegex.test(values.rate.toString()) || values.rate.toString() === 'Nan') { + errors.rate = 'workspace.reimburse.invalidRateError'; + } else if (NumberUtils.parseFloatAnyLocale(values.rate.toString()) <= 0) { + errors.rate = 'workspace.reimburse.lowRateError'; + } + return errors; + }; + + const distanceCustomUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + const distanceCustomRate = Object.values(distanceCustomUnit?.rates ?? {}).find((rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); + + return ( + + {() => ( + + Policy.clearCustomUnitErrors(policy?.id ?? '', distanceCustomUnit?.customUnitID ?? '', distanceCustomRate?.customUnitRateID ?? '')} + > + + + + + + + + )} + + ); +} + +WorkspaceRateAndUnitPage.displayName = 'WorkspaceRateAndUnitPage'; + +export default withPolicy(WorkspaceRateAndUnitPage); diff --git a/src/pages/workspace/reimburse/WorkspaceReimbursePage.js b/src/pages/workspace/reimburse/WorkspaceReimbursePage.js deleted file mode 100644 index fa3849abc941..000000000000 --- a/src/pages/workspace/reimburse/WorkspaceReimbursePage.js +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; -import withPolicy, {policyPropTypes} from '@pages/workspace/withPolicy'; -import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import CONST from '@src/CONST'; -import WorkspaceReimburseView from './WorkspaceReimburseView'; - -const propTypes = { - /** The route object passed to this page from the navigator */ - route: PropTypes.shape({ - /** Each parameter passed via the URL */ - params: PropTypes.shape({ - /** The policyID that is being configured */ - policyID: PropTypes.string.isRequired, - }).isRequired, - }).isRequired, - - ...policyPropTypes, - ...withLocalizePropTypes, -}; - -function WorkspaceReimbursePage(props) { - return ( - - {() => } - - ); -} - -WorkspaceReimbursePage.propTypes = propTypes; -WorkspaceReimbursePage.displayName = 'WorkspaceReimbursePage'; - -export default compose(withPolicy, withLocalize)(WorkspaceReimbursePage); diff --git a/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx b/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx new file mode 100644 index 000000000000..78bf58301db5 --- /dev/null +++ b/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx @@ -0,0 +1,33 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicy from '@pages/workspace/withPolicy'; +import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; +import WorkspaceReimburseView from './WorkspaceReimburseView'; + +type WorkspaceReimbursePageProps = WithPolicyProps & StackScreenProps; + +function WorkspaceReimbursePage({route, policy}: WorkspaceReimbursePageProps) { + const {translate} = useLocalize(); + + return ( + + {() => } + + ); +} + +WorkspaceReimbursePage.displayName = 'WorkspaceReimbursePage'; + +export default withPolicy(WorkspaceReimbursePage); diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseSection.js b/src/pages/workspace/reimburse/WorkspaceReimburseSection.tsx similarity index 57% rename from src/pages/workspace/reimburse/WorkspaceReimburseSection.js rename to src/pages/workspace/reimburse/WorkspaceReimburseSection.tsx index 00ef284c50ae..e4c99d79e324 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseSection.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseSection.tsx @@ -1,45 +1,40 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import ConnectBankAccountButton from '@components/ConnectBankAccountButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; -import networkPropTypes from '@components/networkPropTypes'; import Section from '@components/Section'; import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import BankAccount from '@libs/models/BankAccount'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as Link from '@userActions/Link'; +import type * as OnyxTypes from '@src/types/onyx'; -const propTypes = { +type WorkspaceReimburseSectionProps = { /** Policy values needed in the component */ - policy: PropTypes.shape({ - id: PropTypes.string, - }).isRequired, + policy: OnyxEntry; /** Bank account attached to free plan */ - reimbursementAccount: ReimbursementAccountProps.reimbursementAccountPropTypes.isRequired, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** Returns translated string for given locale and phrase */ - translate: PropTypes.func.isRequired, + reimbursementAccount: OnyxEntry; }; -function WorkspaceReimburseSection(props) { +function WorkspaceReimburseSection({policy, reimbursementAccount}: WorkspaceReimburseSectionProps) { const theme = useTheme(); const styles = useThemeStyles(); + const {translate} = useLocalize(); const [shouldShowLoadingSpinner, setShouldShowLoadingSpinner] = useState(true); - const achState = lodashGet(props.reimbursementAccount, 'achData.state', ''); + const achState = reimbursementAccount?.achData?.state ?? ''; const hasVBA = achState === BankAccount.STATE.OPEN; - const reimburseReceiptsUrl = `reports?policyID=${props.policy.id}&from=all&type=expense&showStates=Archived&isAdvancedFilterMode=true`; - const isLoading = lodashGet(props.reimbursementAccount, 'isLoading', false); + const policyId = policy?.id ?? ''; + const reimburseReceiptsUrl = `reports?policyID=${policyId}&from=all&type=expense&showStates=Archived&isAdvancedFilterMode=true`; + const isLoading = reimbursementAccount?.isLoading ?? false; const prevIsLoading = usePrevious(isLoading); + const {isOffline} = useNetwork(); useEffect(() => { if (prevIsLoading === isLoading) { @@ -48,14 +43,14 @@ function WorkspaceReimburseSection(props) { setShouldShowLoadingSpinner(isLoading); }, [prevIsLoading, isLoading]); - if (props.network.isOffline) { + if (isOffline) { return (
- {`${props.translate('common.youAppearToBeOffline')} ${props.translate('common.thisFeatureRequiresInternet')}`} + {`${translate('common.youAppearToBeOffline')} ${translate('common.thisFeatureRequiresInternet')}`}
); @@ -76,35 +71,35 @@ function WorkspaceReimburseSection(props) { <> {hasVBA ? (
Link.openOldDotLink(reimburseReceiptsUrl), icon: Expensicons.Bank, shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, - wrapperStyle: [styles.cardMenuItem], + wrapperStyle: styles.cardMenuItem, link: () => Link.buildOldDotURL(reimburseReceiptsUrl), }, ]} > - - {props.translate('workspace.reimburse.fastReimbursementsVBACopy')} + + {translate('workspace.reimburse.fastReimbursementsVBACopy')}
) : (
- - {props.translate('workspace.reimburse.unlockNoVBACopy')} + + {translate('workspace.reimburse.unlockNoVBACopy')}
)} @@ -112,7 +107,6 @@ function WorkspaceReimburseSection(props) { ); } -WorkspaceReimburseSection.propTypes = propTypes; WorkspaceReimburseSection.displayName = 'WorkspaceReimburseSection'; export default WorkspaceReimburseSection; diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx similarity index 53% rename from src/pages/workspace/reimburse/WorkspaceReimburseView.js rename to src/pages/workspace/reimburse/WorkspaceReimburseView.tsx index 23136064fc2b..ea9ee9e8a421 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx @@ -1,85 +1,57 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import CopyTextToClipboard from '@components/CopyTextToClipboard'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import networkPropTypes from '@components/networkPropTypes'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; import Section from '@components/Section'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import * as Link from '@userActions/Link'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Unit} from '@src/types/onyx/Policy'; import WorkspaceReimburseSection from './WorkspaceReimburseSection'; -const propTypes = { - /** Policy values needed in the component */ - policy: PropTypes.shape({ - id: PropTypes.string, - customUnits: PropTypes.objectOf( - PropTypes.shape({ - customUnitID: PropTypes.string, - name: PropTypes.string, - attributes: PropTypes.shape({ - unit: PropTypes.string, - }), - rates: PropTypes.objectOf( - PropTypes.shape({ - customUnitRateID: PropTypes.string, - name: PropTypes.string, - rate: PropTypes.number, - }), - ), - }), - ), - outputCurrency: PropTypes.string, - lastModified: PropTypes.number, - }).isRequired, - +type WorkspaceReimburseViewOnyxProps = { /** From Onyx */ /** Bank account attached to free plan */ - reimbursementAccount: ReimbursementAccountProps.reimbursementAccountPropTypes, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - ...withLocalizePropTypes, + reimbursementAccount: OnyxEntry; }; -const defaultProps = { - reimbursementAccount: ReimbursementAccountProps.reimbursementAccountDefaultProps, +type WorkspaceReimburseViewProps = WorkspaceReimburseViewOnyxProps & { + /** Policy values needed in the component */ + policy: OnyxEntry; }; -function WorkspaceReimburseView(props) { +function WorkspaceReimburseView({policy, reimbursementAccount}: WorkspaceReimburseViewProps) { const styles = useThemeStyles(); - const [currentRatePerUnit, setCurrentRatePerUnit] = useState(''); - const viewAllReceiptsUrl = `expenses?policyIDList=${props.policy.id}&billableReimbursable=reimbursable&submitterEmail=%2B%2B`; - const distanceCustomUnit = _.find(lodashGet(props.policy, 'customUnits', {}), (unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); - const distanceCustomRate = _.find(lodashGet(distanceCustomUnit, 'rates', {}), (rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); - const {translate, toLocaleDigit} = props; + const [currentRatePerUnit, setCurrentRatePerUnit] = useState(''); + const viewAllReceiptsUrl = `expenses?policyIDList=${policy?.id ?? ''}&billableReimbursable=reimbursable&submitterEmail=%2B%2B`; + const distanceCustomUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + const distanceCustomRate = Object.values(distanceCustomUnit?.rates ?? {}).find((rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); + const {translate, toLocaleDigit} = useLocalize(); + const {isOffline} = useNetwork(); - const getUnitLabel = useCallback((value) => translate(`common.${value}`), [translate]); + const getUnitLabel = useCallback((value: Unit): string => translate(`common.${value}`), [translate]); const getCurrentRatePerUnitLabel = useCallback(() => { - const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'rates', '{}'), (rate) => rate && rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); - const currentUnit = getUnitLabel(lodashGet(distanceCustomUnit, 'attributes.unit', CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES)); - const currentRate = PolicyUtils.getUnitRateValue(customUnitRate, toLocaleDigit); + const customUnitRate = Object.values(distanceCustomUnit?.rates ?? {}).find((rate) => rate && rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); + const currentUnit = getUnitLabel(distanceCustomUnit?.attributes.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES); + const currentRate = PolicyUtils.getUnitRateValue(toLocaleDigit, customUnitRate); const perWord = translate('common.per'); + return `${currentRate} ${perWord} ${currentUnit}`; }, [translate, distanceCustomUnit, toLocaleDigit, getUnitLabel]); @@ -88,19 +60,19 @@ function WorkspaceReimburseView(props) { // openWorkspaceReimburseView uses API.read which will not make the request until all WRITE requests in the sequential queue have finished responding, so there would be a delay in // updating Onyx with the optimistic data. BankAccounts.setReimbursementAccountLoading(true); - Policy.openWorkspaceReimburseView(props.policy.id); - }, [props.policy.id]); + Policy.openWorkspaceReimburseView(policy?.id ?? ''); + }, [policy?.id]); useEffect(() => { - if (props.network.isOffline) { + if (isOffline) { return; } fetchData(); - }, [props.network.isOffline, fetchData]); + }, [isOffline, fetchData]); useEffect(() => { setCurrentRatePerUnit(getCurrentRatePerUnitLabel()); - }, [props.policy.customUnits, getCurrentRatePerUnitLabel]); + }, [policy?.customUnits, getCurrentRatePerUnitLabel]); return ( <> @@ -114,7 +86,7 @@ function WorkspaceReimburseView(props) { icon: Expensicons.Receipt, shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, - wrapperStyle: [styles.cardMenuItem], + wrapperStyle: styles.cardMenuItem, link: () => Link.buildOldDotURL(viewAllReceiptsUrl), }, ]} @@ -124,7 +96,7 @@ function WorkspaceReimburseView(props) { {translate('workspace.reimburse.captureNoVBACopyBeforeEmail')} {translate('workspace.reimburse.captureNoVBACopyAfterEmail')} @@ -135,44 +107,36 @@ function WorkspaceReimburseView(props) { title={translate('workspace.reimburse.trackDistance')} icon={Illustrations.TrackShoe} > - + {translate('workspace.reimburse.trackDistanceCopy')} Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy.id))} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(policy?.id ?? ''))} wrapperStyle={[styles.mhn5, styles.wAuto]} - brickRoadIndicator={(lodashGet(distanceCustomUnit, 'errors') || lodashGet(distanceCustomRate, 'errors')) && CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR} + brickRoadIndicator={(distanceCustomUnit?.errors ?? distanceCustomRate?.errors) && CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR} /> ); } -WorkspaceReimburseView.defaultProps = defaultProps; -WorkspaceReimburseView.propTypes = propTypes; WorkspaceReimburseView.displayName = 'WorkspaceReimburseView'; -export default compose( - withLocalize, - withNetwork(), - withOnyx({ - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - }, - }), -)(WorkspaceReimburseView); +export default withOnyx({ + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, +})(WorkspaceReimburseView); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 9c6d52a1020d..5c0bb096b42f 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,7 +1,8 @@ import type * as OnyxCommon from './OnyxCommon'; import type PersonalBankAccount from './PersonalBankAccount'; +import type {Unit} from './Policy'; -type FormValueType = string | boolean | Date | OnyxCommon.Errors; +type FormValueType = string | boolean | Date | number | OnyxCommon.Errors; type BaseForm = { /** Controls the loading state of the form */ @@ -59,6 +60,11 @@ type PersonalBankAccountForm = Form; type ReportFieldEditForm = Form>; +type RateUnitForm = Form<{ + unit: Unit; + rate: number; +}>; + export default Form; export type { @@ -73,4 +79,5 @@ export type { IntroSchoolPrincipalForm, PersonalBankAccountForm, ReportFieldEditForm, + RateUnitForm, }; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index fe50bbb497d2..ecc8b1797654 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -10,7 +10,7 @@ type Rate = { currency?: string; customUnitRateID?: string; errors?: OnyxCommon.Errors; - pendingAction?: string; + pendingAction?: OnyxCommon.PendingAction; }; type Attributes = { @@ -22,7 +22,7 @@ type CustomUnit = { customUnitID: string; attributes: Attributes; rates: Record; - pendingAction?: string; + pendingAction?: OnyxCommon.PendingAction; errors?: OnyxCommon.Errors; }; @@ -156,4 +156,4 @@ type Policy = { export default Policy; -export type {Unit, CustomUnit}; +export type {Unit, CustomUnit, Rate, Attributes}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 64eec736b5bf..a2cc5a878b47 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, PrivateNotesForm, ReportFieldEditForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, PrivateNotesForm, RateUnitForm, ReportFieldEditForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -151,5 +151,6 @@ export type { IKnowATeacherForm, IntroSchoolPrincipalForm, PrivateNotesForm, + RateUnitForm, ReportFieldEditForm, }; From f51dfad8cf298537861d378a9336e4598115ce5a Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 1 Feb 2024 10:15:28 +0000 Subject: [PATCH 0147/1113] Revert "[TS Migration] Migrate WorkspaceCard to Typescript" This reverts commit 4feb09259b4501462807da3bb197aa705e3c1d4f. --- .../workspace/card/WorkspaceCardNoVBAView.js | 49 +++++++++++++++++++ .../workspace/card/WorkspaceCardNoVBAView.tsx | 40 --------------- src/pages/workspace/card/WorkspaceCardPage.js | 47 ++++++++++++++++++ .../workspace/card/WorkspaceCardPage.tsx | 39 --------------- ...iew.tsx => WorkspaceCardVBANoECardView.js} | 48 +++++++++++------- ...w.tsx => WorkspaceCardVBAWithECardView.js} | 39 +++++++-------- 6 files changed, 145 insertions(+), 117 deletions(-) create mode 100644 src/pages/workspace/card/WorkspaceCardNoVBAView.js delete mode 100644 src/pages/workspace/card/WorkspaceCardNoVBAView.tsx create mode 100644 src/pages/workspace/card/WorkspaceCardPage.js delete mode 100644 src/pages/workspace/card/WorkspaceCardPage.tsx rename src/pages/workspace/card/{WorkspaceCardVBANoECardView.tsx => WorkspaceCardVBANoECardView.js} (53%) rename src/pages/workspace/card/{WorkspaceCardVBAWithECardView.tsx => WorkspaceCardVBAWithECardView.js} (67%) diff --git a/src/pages/workspace/card/WorkspaceCardNoVBAView.js b/src/pages/workspace/card/WorkspaceCardNoVBAView.js new file mode 100644 index 000000000000..3233f8ea7e23 --- /dev/null +++ b/src/pages/workspace/card/WorkspaceCardNoVBAView.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {View} from 'react-native'; +import ConnectBankAccountButton from '@components/ConnectBankAccountButton'; +import * as Illustrations from '@components/Icon/Illustrations'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import UnorderedList from '@components/UnorderedList'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +const propTypes = { + /** The policy ID currently being configured */ + policyID: PropTypes.string.isRequired, + + ...withLocalizePropTypes, +}; + +function WorkspaceCardNoVBAView(props) { + const styles = useThemeStyles(); + return ( +
+ + {props.translate('workspace.card.noVBACopy')} + + + + +
+ ); +} + +WorkspaceCardNoVBAView.propTypes = propTypes; +WorkspaceCardNoVBAView.displayName = 'WorkspaceCardNoVBAView'; + +export default withLocalize(WorkspaceCardNoVBAView); diff --git a/src/pages/workspace/card/WorkspaceCardNoVBAView.tsx b/src/pages/workspace/card/WorkspaceCardNoVBAView.tsx deleted file mode 100644 index 322d433a8e62..000000000000 --- a/src/pages/workspace/card/WorkspaceCardNoVBAView.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import ConnectBankAccountButton from '@components/ConnectBankAccountButton'; -import * as Illustrations from '@components/Icon/Illustrations'; -import Section from '@components/Section'; -import Text from '@components/Text'; -import UnorderedList from '@components/UnorderedList'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type WorkspaceCardNoVBAViewProps = { - /** The policy ID currently being configured */ - policyID: string; -}; - -function WorkspaceCardNoVBAView({policyID}: WorkspaceCardNoVBAViewProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - return ( -
- - {translate('workspace.card.noVBACopy')} - - - - -
- ); -} - -WorkspaceCardNoVBAView.displayName = 'WorkspaceCardNoVBAView'; - -export default WorkspaceCardNoVBAView; diff --git a/src/pages/workspace/card/WorkspaceCardPage.js b/src/pages/workspace/card/WorkspaceCardPage.js new file mode 100644 index 000000000000..55220b85ce63 --- /dev/null +++ b/src/pages/workspace/card/WorkspaceCardPage.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import CONST from '@src/CONST'; +import WorkspaceCardNoVBAView from './WorkspaceCardNoVBAView'; +import WorkspaceCardVBANoECardView from './WorkspaceCardVBANoECardView'; +import WorkspaceCardVBAWithECardView from './WorkspaceCardVBAWithECardView'; + +const propTypes = { + /** The route object passed to this page from the navigator */ + route: PropTypes.shape({ + /** Each parameter passed via the URL */ + params: PropTypes.shape({ + /** The policyID that is being configured */ + policyID: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + + ...withLocalizePropTypes, +}; + +function WorkspaceCardPage(props) { + return ( + + {(hasVBA, policyID, isUsingECard) => ( + <> + {!hasVBA && } + + {hasVBA && !isUsingECard && } + + {hasVBA && isUsingECard && } + + )} + + ); +} + +WorkspaceCardPage.propTypes = propTypes; +WorkspaceCardPage.displayName = 'WorkspaceCardPage'; + +export default withLocalize(WorkspaceCardPage); diff --git a/src/pages/workspace/card/WorkspaceCardPage.tsx b/src/pages/workspace/card/WorkspaceCardPage.tsx deleted file mode 100644 index f6e368db84ea..000000000000 --- a/src/pages/workspace/card/WorkspaceCardPage.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; -import useLocalize from '@hooks/useLocalize'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import CONST from '@src/CONST'; -import type SCREENS from '@src/SCREENS'; -import WorkspaceCardNoVBAView from './WorkspaceCardNoVBAView'; -import WorkspaceCardVBANoECardView from './WorkspaceCardVBANoECardView'; -import WorkspaceCardVBAWithECardView from './WorkspaceCardVBAWithECardView'; - -type WorkspaceCardPageProps = StackScreenProps; - -function WorkspaceCardPage({route}: WorkspaceCardPageProps) { - const {translate} = useLocalize(); - - return ( - - {(hasVBA: boolean, policyID: string, isUsingECard: boolean) => ( - <> - {false && } - - {false && } - - {true && } - - )} - - ); -} - -WorkspaceCardPage.displayName = 'WorkspaceCardPage'; - -export default WorkspaceCardPage; diff --git a/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx b/src/pages/workspace/card/WorkspaceCardVBANoECardView.js similarity index 53% rename from src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx rename to src/pages/workspace/card/WorkspaceCardVBANoECardView.js index 3c9b773d6994..970cd9105368 100644 --- a/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx +++ b/src/pages/workspace/card/WorkspaceCardVBANoECardView.js @@ -1,6 +1,5 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -8,37 +7,45 @@ import * as Illustrations from '@components/Icon/Illustrations'; import Section from '@components/Section'; import Text from '@components/Text'; import UnorderedList from '@components/UnorderedList'; -import useLocalize from '@hooks/useLocalize'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import compose from '@libs/compose'; +import userPropTypes from '@pages/settings/userPropTypes'; import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {User} from '@src/types/onyx'; -type WorkspaceCardVBANoECardViewOnyxProps = { +const propTypes = { /** Information about the logged in user's account */ - user: OnyxEntry; + user: userPropTypes, + + ...withLocalizePropTypes, }; -type WorkspaceCardVBANoECardViewProps = WorkspaceCardVBANoECardViewOnyxProps; +const defaultProps = { + user: {}, +}; -function WorkspaceCardVBANoECardView({user}: WorkspaceCardVBANoECardViewProps) { +function WorkspaceCardVBANoECardView(props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); - return ( <>
- {Boolean(user?.isCheckingDomain) && {translate('workspace.card.checkingDomain')}} + {Boolean(props.user.isCheckingDomain) && {props.translate('workspace.card.checkingDomain')}} ); } +WorkspaceCardVBANoECardView.propTypes = propTypes; +WorkspaceCardVBANoECardView.defaultProps = defaultProps; WorkspaceCardVBANoECardView.displayName = 'WorkspaceCardVBANoECardView'; -export default withOnyx({ - user: { - key: ONYXKEYS.USER, - }, -})(WorkspaceCardVBANoECardView); +export default compose( + withLocalize, + withOnyx({ + user: { + key: ONYXKEYS.USER, + }, + }), +)(WorkspaceCardVBANoECardView); diff --git a/src/pages/workspace/card/WorkspaceCardVBAWithECardView.tsx b/src/pages/workspace/card/WorkspaceCardVBAWithECardView.js similarity index 67% rename from src/pages/workspace/card/WorkspaceCardVBAWithECardView.tsx rename to src/pages/workspace/card/WorkspaceCardVBAWithECardView.js index a53a44fa52cf..40ecd80b8e6e 100644 --- a/src/pages/workspace/card/WorkspaceCardVBAWithECardView.tsx +++ b/src/pages/workspace/card/WorkspaceCardVBAWithECardView.js @@ -2,35 +2,28 @@ import React from 'react'; import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; -import type {MenuItemWithLink} from '@components/MenuItemList'; import Section from '@components/Section'; import Text from '@components/Text'; import UnorderedList from '@components/UnorderedList'; -import useLocalize from '@hooks/useLocalize'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Link from '@userActions/Link'; -type MenuLinks = { - ISSUE_AND_MANAGE_CARDS: string; - RECONCILE_CARDS: string; - SETTLEMENT_FREQUENCY: string; +const propTypes = { + ...withLocalizePropTypes, }; -type MenuItems = MenuItemWithLink[]; - -const MENU_LINKS: MenuLinks = { +const MENU_LINKS = { ISSUE_AND_MANAGE_CARDS: 'domain_companycards', RECONCILE_CARDS: encodeURI('domain_companycards?param={"section":"cardReconciliation"}'), SETTLEMENT_FREQUENCY: encodeURI('domain_companycards?param={"section":"configureSettings"}'), }; -function WorkspaceCardVBAWithECardView() { +function WorkspaceCardVBAWithECardView(props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); - - const menuItems: MenuItems = [ + const menuItems = [ { - title: translate('workspace.common.issueAndManageCards'), + title: props.translate('workspace.common.issueAndManageCards'), onPress: () => Link.openOldDotLink(MENU_LINKS.ISSUE_AND_MANAGE_CARDS), icon: Expensicons.ExpensifyCard, shouldShowRightIcon: true, @@ -39,7 +32,7 @@ function WorkspaceCardVBAWithECardView() { link: () => Link.buildOldDotURL(MENU_LINKS.ISSUE_AND_MANAGE_CARDS), }, { - title: translate('workspace.common.reconcileCards'), + title: props.translate('workspace.common.reconcileCards'), onPress: () => Link.openOldDotLink(MENU_LINKS.RECONCILE_CARDS), icon: Expensicons.ReceiptSearch, shouldShowRightIcon: true, @@ -48,7 +41,7 @@ function WorkspaceCardVBAWithECardView() { link: () => Link.buildOldDotURL(MENU_LINKS.RECONCILE_CARDS), }, { - title: translate('workspace.common.settlementFrequency'), + title: props.translate('workspace.common.settlementFrequency'), onPress: () => Link.openOldDotLink(MENU_LINKS.SETTLEMENT_FREQUENCY), icon: Expensicons.Gear, shouldShowRightIcon: true, @@ -60,23 +53,29 @@ function WorkspaceCardVBAWithECardView() { return (
- {translate('workspace.card.VBAWithECardCopy')} + {props.translate('workspace.card.VBAWithECardCopy')}
); } +WorkspaceCardVBAWithECardView.propTypes = propTypes; WorkspaceCardVBAWithECardView.displayName = 'WorkspaceCardVBAWithECardView'; -export default WorkspaceCardVBAWithECardView; +export default withLocalize(WorkspaceCardVBAWithECardView); From 3a7ad1e1314b598cd5acdd012ac15ba4890a2112 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 1 Feb 2024 10:20:07 +0000 Subject: [PATCH 0148/1113] [TS migration][WorkspaceReimburse] Missing imports --- src/libs/actions/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 48fd8944f196..c8f982a0c6c6 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -37,7 +37,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, PolicyMember, PolicyTags, RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, Report, ReportAction, Transaction} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; -import type {CustomUnit} from '@src/types/onyx/Policy'; +import type {Attributes, CustomUnit, Rate} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AnnounceRoomMembersOnyxData = { From 28eae1dd56a9d69ba0753c4794cfd1b88ef5225a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 1 Feb 2024 17:26:12 +0100 Subject: [PATCH 0149/1113] 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 d11c98a08b95289c93bf2824f9aff85ee9741c1d Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Fri, 2 Feb 2024 09:39:11 +0000 Subject: [PATCH 0150/1113] [TS migration][WorkspaceReimburse] Code improvements --- .../parameters/UpdateWorkspaceCustomUnitAndRateParams.ts | 2 +- src/libs/PolicyUtils.ts | 2 +- src/libs/actions/Policy.ts | 2 +- .../workspace/reimburse/WorkspaceRateAndUnitPage.tsx | 8 ++++---- src/pages/workspace/reimburse/WorkspaceReimburseView.tsx | 1 - src/types/onyx/Form.ts | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts index 22bbd20c7308..02186cddd32a 100644 --- a/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts +++ b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts @@ -1,6 +1,6 @@ type UpdateWorkspaceCustomUnitAndRateParams = { policyID: string; - lastModified: number; + lastModified?: number; customUnit: string; customUnitRate: string; }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index a812cab24402..b4270867f9b2 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -50,7 +50,7 @@ function hasCustomUnitsError(policy: OnyxEntry): boolean { return Object.keys(policy?.customUnits?.errors ?? {}).length > 0; } -function getNumericValue(value: number, toLocaleDigit: (arg: string) => string): number | string { +function getNumericValue(value: number | string, toLocaleDigit: (arg: string) => string): number | string { const numValue = parseFloat(value.toString().replace(toLocaleDigit('.'), '.')); if (Number.isNaN(numValue)) { return NaN; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index c8f982a0c6c6..1c9295dc08bc 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -939,7 +939,7 @@ function hideWorkspaceAlertMessage(policyID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {alertMessage: ''}); } -function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: NewCustomUnit, lastModified: number) { +function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: NewCustomUnit, lastModified?: string) { if (!currentCustomUnit.customUnitID || !newCustomUnit?.customUnitID || !newCustomUnit.rates?.customUnitRateID) { return; } diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx index 9c24b6bde023..1f5d7d854046 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx @@ -48,7 +48,7 @@ function WorkspaceRateAndUnitPage({policy, route}: WorkspaceRateAndUnitPageProps {label: translate('common.miles'), value: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, ]; - const saveUnitAndRate = (unit: Unit, rate: number) => { + const saveUnitAndRate = (unit: Unit, rate: string) => { const distanceCustomUnit = Object.values(policy?.customUnits ?? {}).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); if (!distanceCustomUnit) { return; @@ -68,7 +68,7 @@ function WorkspaceRateAndUnitPage({policy, route}: WorkspaceRateAndUnitPageProps }, }; - Policy.updateWorkspaceCustomUnitAndRate(policy?.id ?? '', distanceCustomUnit, newCustomUnit, parseInt(policy?.lastModified ?? '', 2)); + Policy.updateWorkspaceCustomUnitAndRate(policy?.id ?? '', distanceCustomUnit, newCustomUnit, policy?.lastModified); }; const submit = (values: OnyxFormValuesFields) => { @@ -83,9 +83,9 @@ function WorkspaceRateAndUnitPage({policy, route}: WorkspaceRateAndUnitPageProps const outputCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; // Allow one more decimal place for accuracy const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,${CurrencyUtils.getCurrencyDecimals(outputCurrency) + 1}})?$`, 'i'); - if (!rateValueRegex.test(values.rate.toString()) || values.rate.toString() === 'Nan') { + if (!rateValueRegex.test(values.rate) || values.rate === '') { errors.rate = 'workspace.reimburse.invalidRateError'; - } else if (NumberUtils.parseFloatAnyLocale(values.rate.toString()) <= 0) { + } else if (NumberUtils.parseFloatAnyLocale(values.rate) <= 0) { errors.rate = 'workspace.reimburse.lowRateError'; } return errors; diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx b/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx index ea9ee9e8a421..375fa5b8d439 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx @@ -25,7 +25,6 @@ import type {Unit} from '@src/types/onyx/Policy'; import WorkspaceReimburseSection from './WorkspaceReimburseSection'; type WorkspaceReimburseViewOnyxProps = { - /** From Onyx */ /** Bank account attached to free plan */ reimbursementAccount: OnyxEntry; }; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 5c0bb096b42f..1b119460b344 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -62,7 +62,7 @@ type ReportFieldEditForm = Form>; type RateUnitForm = Form<{ unit: Unit; - rate: number; + rate: string; }>; export default Form; From 150e9a4b3e1be297b4c19bc01eb9f5fac0c66c08 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 2 Feb 2024 10:55:38 +0100 Subject: [PATCH 0151/1113] 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 0152/1113] 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 0153/1113] 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 9bd67d67aee253e4614ef64854169229282f806d Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 5 Feb 2024 16:14:46 +0530 Subject: [PATCH 0154/1113] merge main, fix lint --- src/pages/EnablePayments/ActivateStep.tsx | 2 +- .../EnablePayments/EnablePaymentsPage.tsx | 3 ++- src/pages/EnablePayments/OnfidoPrivacy.tsx | 23 +++++++++++-------- src/pages/EnablePayments/OnfidoStep.tsx | 2 +- .../TermsPage/LongTermsForm.tsx | 1 - .../TermsPage/ShortTermsForm.tsx | 2 +- src/pages/EnablePayments/TermsStep.tsx | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/pages/EnablePayments/ActivateStep.tsx b/src/pages/EnablePayments/ActivateStep.tsx index 0dbb98e53a5f..e0bea7488140 100644 --- a/src/pages/EnablePayments/ActivateStep.tsx +++ b/src/pages/EnablePayments/ActivateStep.tsx @@ -8,7 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {UserWallet, WalletTerms} from '@src/types/onyx'; +import type {UserWallet, WalletTerms} from '@src/types/onyx'; type ActivateStepOnyxProps = { /** Information about the user accepting the terms for payments */ diff --git a/src/pages/EnablePayments/EnablePaymentsPage.tsx b/src/pages/EnablePayments/EnablePaymentsPage.tsx index 6d2defc82df0..1384875fe031 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.tsx +++ b/src/pages/EnablePayments/EnablePaymentsPage.tsx @@ -25,7 +25,7 @@ type EnablePaymentsPageOnyxProps = { userWallet: OnyxEntry; }; -type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps & {}; +type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps; function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { const {translate} = useLocalize(); @@ -38,6 +38,7 @@ function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { return; } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (isPendingOnfidoResult || hasFailedOnfido) { Navigation.navigate(ROUTES.SETTINGS_WALLET, CONST.NAVIGATION.TYPE.UP); return; diff --git a/src/pages/EnablePayments/OnfidoPrivacy.tsx b/src/pages/EnablePayments/OnfidoPrivacy.tsx index d8e9f616b8a7..80fa08c009ca 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.tsx +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -1,9 +1,8 @@ -import lodashGet from 'lodash/get'; import React, {useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; +import type {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import _ from 'underscore'; import FixedFooter from '@components/FixedFooter'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormScrollView from '@components/FormScrollView'; @@ -15,7 +14,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import {WalletOnfido} from '@src/types/onyx'; +import type {WalletOnfido} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; const DEFAULT_WALLET_ONFIDO_DATA = { applicantID: '', @@ -31,22 +31,25 @@ type OnfidoPrivacyOnyxProps = { walletOnfidoData: OnyxEntry; }; -type OnfidoPrivacyProps = OnfidoPrivacyOnyxProps & {}; +type OnfidoPrivacyProps = OnfidoPrivacyOnyxProps; function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPrivacyProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData ?? {}; - const formRef = useRef(null); + const styles = useThemeStyles(); + if (!walletOnfidoData) { + return; + } + const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData; const openOnfidoFlow = () => { BankAccounts.openOnfidoFlow(); }; let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) || ''; - const onfidoFixableErrors = lodashGet(walletOnfidoData, 'fixableErrors', []); - onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + + const onfidoFixableErrors = walletOnfidoData?.fixableErrors ?? []; + onfidoError += !isEmptyObject(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; return ( diff --git a/src/pages/EnablePayments/OnfidoStep.tsx b/src/pages/EnablePayments/OnfidoStep.tsx index 5e36f2f302a6..04cadb24fecf 100644 --- a/src/pages/EnablePayments/OnfidoStep.tsx +++ b/src/pages/EnablePayments/OnfidoStep.tsx @@ -13,7 +13,7 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {WalletOnfido} from '@src/types/onyx'; +import type {WalletOnfido} from '@src/types/onyx'; import OnfidoPrivacy from './OnfidoPrivacy'; const DEFAULT_WALLET_ONFIDO_DATA = { diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx index ec89856642d9..81d18c5dfc44 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import CollapsibleSection from '@components/CollapsibleSection'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx index b45b8b657a75..f4db904c07f3 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx @@ -7,7 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; -import {UserWallet} from '@src/types/onyx'; +import type {UserWallet} from '@src/types/onyx'; type ShortTermsFormProps = { /** The user's wallet */ diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index cef54ba463e6..fe463f88792d 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -12,7 +12,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import {UserWallet, WalletTerms} from '@src/types/onyx'; +import type {UserWallet, WalletTerms} from '@src/types/onyx'; import LongTermsForm from './TermsPage/LongTermsForm'; import ShortTermsForm from './TermsPage/ShortTermsForm'; From c0f4f030db1ff9e61da920b9c4393a82cf42a59b Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 5 Feb 2024 19:22:28 +0530 Subject: [PATCH 0155/1113] TS-migration: IdologyQuestions Page --- src/ONYXKEYS.ts | 4 + ...ologyQuestions.js => IdologyQuestions.tsx} | 81 +++++++------------ src/types/onyx/Form.ts | 5 ++ src/types/onyx/index.ts | 2 + 4 files changed, 39 insertions(+), 53 deletions(-) rename src/pages/EnablePayments/{IdologyQuestions.js => IdologyQuestions.tsx} (69%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7328fb2543ad..3046b5a16bdd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -367,6 +367,8 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm', PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', + IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', + IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', }, } as const; @@ -556,6 +558,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_DRAFT]: OnyxTypes.PersonalBankAccount; + [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: OnyxTypes.IdologyQuestionsForm; + [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM_DRAFT]: OnyxTypes.IdologyQuestionsForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.tsx similarity index 69% rename from src/pages/EnablePayments/IdologyQuestions.js rename to src/pages/EnablePayments/IdologyQuestions.tsx index a0c202b0bbbc..4a5ef72019c0 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -1,10 +1,10 @@ -import PropTypes from 'prop-types'; import React, {useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {WalletAdditionalQuestionDetails} from 'src/types/onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; +import type {Choice} from '@components/RadioButtons'; import SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; @@ -12,53 +12,36 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; -const propTypes = { +type IdologyQuestionsProps = { /** Questions returned by Idology */ /** example: [{"answer":["1251","6253","113","None of the above","Skip Question"],"prompt":"Which number goes with your address on MASONIC AVE?","type":"street.number.b"}, ...] */ - questions: PropTypes.arrayOf( - PropTypes.shape({ - prompt: PropTypes.string, - type: PropTypes.string, - answer: PropTypes.arrayOf(PropTypes.string), - }), - ), + questions: WalletAdditionalQuestionDetails[]; /** ID from Idology, referencing those questions */ - idNumber: PropTypes.string, - - walletAdditionalDetails: PropTypes.shape({ - /** Are we waiting for a response? */ - isLoading: PropTypes.bool, - - /** Any additional error message to show */ - errors: PropTypes.objectOf(PropTypes.string), - - /** What error do we need to handle */ - errorCode: PropTypes.string, - }), + idNumber: string; }; -const defaultProps = { - questions: [], - idNumber: '', - walletAdditionalDetails: {}, +type Answer = { + question: string; + answer: string; }; -function IdologyQuestions({questions, idNumber}) { +function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [shouldHideSkipAnswer, setShouldHideSkipAnswer] = useState(false); - const [userAnswers, setUserAnswers] = useState([]); + const [userAnswers, setUserAnswers] = useState([]); const currentQuestion = questions[currentQuestionIndex] || {}; - const possibleAnswers = _.filter( - _.map(currentQuestion.answer, (answer) => { + const possibleAnswers: Choice[] = currentQuestion.answer + .map((answer) => { if (shouldHideSkipAnswer && answer === SKIP_QUESTION_TEXT) { return; } @@ -67,15 +50,11 @@ function IdologyQuestions({questions, idNumber}) { label: answer, value: answer, }; - }), - ); + }) + .filter((answer): answer is Choice => answer !== undefined); - /** - * Put question answer in the state. - * @param {String} answer - */ - const chooseAnswer = (answer) => { - const tempAnswers = _.map(userAnswers, _.clone); + const chooseAnswer = (answer: string) => { + const tempAnswers: Answer[] = userAnswers.map((userAnswer) => ({...userAnswer})); tempAnswers[currentQuestionIndex] = {question: currentQuestion.type, answer}; @@ -90,11 +69,11 @@ function IdologyQuestions({questions, idNumber}) { return; } // Get the number of questions that were skipped by the user. - const skippedQuestionsCount = _.filter(userAnswers, (answer) => answer.answer === SKIP_QUESTION_TEXT).length; + const skippedQuestionsCount = userAnswers.filter((answer) => answer.answer === SKIP_QUESTION_TEXT).length; // We have enough answers, let's call expectID KBA to verify them if (userAnswers.length - skippedQuestionsCount >= questions.length - MAX_SKIP) { - const tempAnswers = _.map(userAnswers, _.clone); + const tempAnswers: Answer[] = userAnswers.map((answer) => ({...answer})); // Auto skip any remaining questions if (tempAnswers.length < questions.length) { @@ -112,8 +91,8 @@ function IdologyQuestions({questions, idNumber}) { } }; - const validate = (values) => { - const errors = {}; + const validate = (values: OnyxFormValuesFields) => { + const errors: Errors = {}; if (!values.answer) { errors.answer = translate('additionalDetailsStep.selectAnswer'); } @@ -132,7 +111,7 @@ function IdologyQuestions({questions, idNumber}) {
@@ -155,11 +136,5 @@ function IdologyQuestions({questions, idNumber}) { } IdologyQuestions.displayName = 'IdologyQuestions'; -IdologyQuestions.propTypes = propTypes; -IdologyQuestions.defaultProps = defaultProps; - -export default withOnyx({ - walletAdditionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, -})(IdologyQuestions); + +export default IdologyQuestions; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 48f386afcbb0..79190aa3e6f6 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -68,6 +68,10 @@ type CloseAccountForm = Form<{ phoneOrEmail: string; }>; +type IdologyQuestionsForm = Form<{ + answer: string; +}>; + export default Form; export type { @@ -84,4 +88,5 @@ export type { WorkspaceSettingsForm, ReportFieldEditForm, CloseAccountForm, + IdologyQuestionsForm, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index d0ac2ce395fa..13c305abe384 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -14,6 +14,7 @@ import type { CloseAccountForm, DateOfBirthForm, DisplayNameForm, + IdologyQuestionsForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, @@ -165,4 +166,5 @@ export type { IntroSchoolPrincipalForm, PrivateNotesForm, ReportFieldEditForm, + IdologyQuestionsForm, }; From 61d24b89c0132dd7ce1049a62b0e88e9814b3c5f Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 5 Feb 2024 21:29:42 +0530 Subject: [PATCH 0156/1113] TS-migration: AdditionalDetailsStep Page --- src/ONYXKEYS.ts | 4 + .../withCurrentUserPersonalDetails.tsx | 2 +- src/libs/actions/PersonalDetails.ts | 3 +- ...tailsStep.js => AdditionalDetailsStep.tsx} | 120 ++++++------------ src/types/onyx/Form.ts | 13 ++ src/types/onyx/index.ts | 2 + 6 files changed, 64 insertions(+), 80 deletions(-) rename src/pages/EnablePayments/{AdditionalDetailsStep.js => AdditionalDetailsStep.tsx} (73%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 3046b5a16bdd..2e4e5b564616 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -369,6 +369,8 @@ const ONYXKEYS = { PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', + ADDITIONAL_DETAILS_FORM: 'additionalDetailStep', + ADDITIONAL_DETAILS_FORM_DRAFT: 'additionalDetailStepDraft', }, } as const; @@ -560,6 +562,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_DRAFT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: OnyxTypes.IdologyQuestionsForm; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM_DRAFT]: OnyxTypes.IdologyQuestionsForm; + [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM]: OnyxTypes.AdditionalDetailStepForm; + [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM_DRAFT]: OnyxTypes.AdditionalDetailStepForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 9406c8634c1b..75bdb03ea6d8 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -64,4 +64,4 @@ export default function ; }; -const defaultProps = { - walletAdditionalDetails: { - errorFields: {}, - isLoading: false, - errors: {}, - questions: [], - idNumber: '', - errorCode: '', - }, - ...withCurrentUserPersonalDetailsDefaultProps, -}; +type AdditionalDetailsStepProps = AdditionalDetailsStepOnyxProps & WithCurrentUserPersonalDetailsProps; const fieldNameTranslationKeys = { legalFirstName: 'additionalDetailsStep.legalFirstNameLabel', @@ -77,20 +50,17 @@ const fieldNameTranslationKeys = { dob: 'common.dob', ssn: 'common.ssnLast4', ssnFull9: 'common.ssnFull9', -}; +} as const; -function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserPersonalDetails}) { +function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIONAL_DETAILS, currentUserPersonalDetails}: AdditionalDetailsStepProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); const currentDate = new Date(); const minDate = subYears(currentDate, CONST.DATE_BIRTH.MAX_AGE); const maxDate = subYears(currentDate, CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); - const shouldAskForFullSSN = walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN; + const shouldAskForFullSSN = walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN; - /** - * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID - * @returns {Object} - */ - const validate = (values) => { + const validate = (values: OnyxFormValuesFields) => { const requiredFields = ['legalFirstName', 'legalLastName', 'addressStreet', 'addressCity', 'addressZipCode', 'phoneNumber', 'dob', 'ssn', 'addressState']; const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); @@ -116,7 +86,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP // walletAdditionalDetails stores errors returned by the server. If the server returns an SSN error // then the user needs to provide the full 9 digit SSN. - if (walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN) { + if (walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN) { if (values.ssn && !ValidationUtils.isValidSSNFullNine(values.ssn)) { errors.ssn = 'additionalDetailsStep.ssnFull9Error'; } @@ -127,26 +97,23 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP return errors; }; - /** - * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID - */ - const activateWallet = (values) => { + const activateWallet = (values: OnyxFormValuesFields) => { const personalDetails = { - phoneNumber: parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number.significant || '', - legalFirstName: values.legalFirstName || '', - legalLastName: values.legalLastName || '', - addressStreet: values.addressStreet || '', - addressCity: values.addressCity || '', - addressState: values.addressState || '', - addressZip: values.addressZipCode || '', - dob: values.dob || '', - ssn: values.ssn || '', + phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', + legalFirstName: values.legalFirstName ?? '', + legalLastName: values.legalLastName ?? '', + addressStreet: values.addressStreet ?? '', + addressCity: values.addressCity ?? '', + addressState: values.addressState ?? '', + addressZip: values.addressZipCode ?? '', + dob: values.dob ?? '', + ssn: values.ssn ?? '', }; // Attempt to set the personal details Wallet.updatePersonalDetails(personalDetails); }; - if (!_.isEmpty(walletAdditionalDetails.questions)) { + if (walletAdditionalDetails?.questions && walletAdditionalDetails.questions.length > 0) { return ( Wallet.setAdditionalDetailsQuestions(null)} + onBackButtonPress={() => Wallet.setAdditionalDetailsQuestions([], walletAdditionalDetails?.idNumber ?? '')} /> ); @@ -180,7 +147,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP
({ walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, - }), -)(AdditionalDetailsStep); + })(AdditionalDetailsStep), +); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 79190aa3e6f6..646950dcb986 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -72,6 +72,18 @@ type IdologyQuestionsForm = Form<{ answer: string; }>; +type AdditionalDetailStepForm = Form<{ + legalFirstName: string; + legalLastName: string; + addressStreet: string; + addressCity: string; + addressZipCode: string; + phoneNumber: string; + dob: string; + ssn: string; + addressState: string; +}>; + export default Form; export type { @@ -89,4 +101,5 @@ export type { ReportFieldEditForm, CloseAccountForm, IdologyQuestionsForm, + AdditionalDetailStepForm, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 13c305abe384..ff6c782a37e1 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,6 +11,7 @@ import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; import type { AddDebitCardForm, + AdditionalDetailStepForm, CloseAccountForm, DateOfBirthForm, DisplayNameForm, @@ -167,4 +168,5 @@ export type { PrivateNotesForm, ReportFieldEditForm, IdologyQuestionsForm, + AdditionalDetailStepForm, }; From 1ad742de9d5b27113e74d85691016089ff0d89ae Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Tue, 6 Feb 2024 08:08:34 -0800 Subject: [PATCH 0157/1113] Fix prettier formatting --- src/libs/ReportUtils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 09496c6b18f1..e485cf615a41 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2257,7 +2257,14 @@ function getReportPreviewMessage( }); } - if (!isEmptyObject(linkedTransaction) && !isEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction) && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if ( + !isEmptyObject(linkedTransaction) && + !isEmptyObject(reportAction) && + shouldConsiderReceiptBeingScanned && + TransactionUtils.hasReceipt(linkedTransaction) && + TransactionUtils.isReceiptBeingScanned(linkedTransaction) && + ReportActionsUtils.isMoneyRequestAction(reportAction) + ) { return Localize.translateLocal('iou.receiptScanning'); } From 9ff4961a40c9eb4125207b5aa89852f68f9f2a81 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 7 Feb 2024 07:55:32 +0530 Subject: [PATCH 0158/1113] fix: animated background getting cropped on android --- src/pages/home/report/AnimatedEmptyStateBackground.tsx | 2 +- src/pages/home/report/ReportActionItem.js | 4 ++-- src/styles/utils/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/AnimatedEmptyStateBackground.tsx b/src/pages/home/report/AnimatedEmptyStateBackground.tsx index 7e259b7473cf..5e91aada8464 100644 --- a/src/pages/home/report/AnimatedEmptyStateBackground.tsx +++ b/src/pages/home/report/AnimatedEmptyStateBackground.tsx @@ -13,7 +13,7 @@ function AnimatedEmptyStateBackground() { const StyleUtils = useStyleUtils(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const illustrations = useThemeIllustrations(); - const IMAGE_OFFSET_X = windowWidth / 2; + const IMAGE_OFFSET_X = 0; // If window width is greater than the max background width, repeat the background image const maxBackgroundWidth = variables.sideBarWidth + CONST.EMPTY_STATE_BACKGROUND.ASPECT_RATIO * CONST.EMPTY_STATE_BACKGROUND.WIDE_SCREEN.IMAGE_HEIGHT; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 435c086d913f..75e9e1ca8627 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -583,7 +583,7 @@ function ReportActionItem(props) { if (ReportUtils.isTaskReport(props.report)) { if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { return ( - <> + - + ); } return ( diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index b3b4924ebb19..8657594f0a10 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -701,7 +701,7 @@ function getReportWelcomeBackgroundImageStyle(isSmallScreenWidth: boolean, isMon if (isSmallScreenWidth) { return { height: emptyStateBackground.SMALL_SCREEN.IMAGE_HEIGHT, - width: '200%', + width: '100%', position: 'absolute', }; } From 17c4fba70c1d4f8a9aaebd8e7a7c045227ba83ab Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 7 Feb 2024 11:27:38 +0100 Subject: [PATCH 0159/1113] 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 0160/1113] 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 5f89ad6cf8fdcc0a4a5522a35588a36c40670f70 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Thu, 8 Feb 2024 17:16:50 +0530 Subject: [PATCH 0161/1113] merge main, lint fixed and prettified code --- src/libs/PersonalDetailsUtils.ts | 2 +- src/pages/EnablePayments/OnfidoPrivacy.tsx | 2 +- src/pages/EnablePayments/TermsStep.tsx | 2 +- src/types/onyx/Form.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index aa9a7f79e582..69b0f386a76c 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -1,11 +1,11 @@ import Str from 'expensify-common/lib/str'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; diff --git a/src/pages/EnablePayments/OnfidoPrivacy.tsx b/src/pages/EnablePayments/OnfidoPrivacy.tsx index 80fa08c009ca..5d8cdc751e87 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.tsx +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -46,7 +46,7 @@ function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPr BankAccounts.openOnfidoFlow(); }; - let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) || ''; + let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) ?? ''; const onfidoFixableErrors = walletOnfidoData?.fixableErrors ?? []; onfidoError += !isEmptyObject(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index fe463f88792d..4836feae9f9b 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -33,7 +33,7 @@ function TermsStep(props: TermsStepProps) { const [error, setError] = useState(false); const {translate} = useLocalize(); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) || ''; + const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) ?? ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index bb78487892bf..7043449ae6cf 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -82,7 +82,7 @@ type AdditionalDetailStepForm = Form<{ dob: string; ssn: string; addressState: string; -}> +}>; type RoomNameForm = Form<{ roomName: string; From 34135a2c023764b456df2fe86a0c95af2cb26022 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 8 Feb 2024 15:54:07 +0000 Subject: [PATCH 0162/1113] [TS migration][WorkspaceReimburse] Code improvements --- .../UpdateWorkspaceCustomUnitAndRateParams.ts | 2 +- .../reimburse/WorkspaceRateAndUnitPage.tsx | 4 ++-- .../workspace/reimburse/WorkspaceReimburseView.tsx | 13 ------------- src/types/onyx/Form.ts | 2 +- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts index a94089512a36..010bcaa1e60a 100644 --- a/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts +++ b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts @@ -1,6 +1,6 @@ type UpdateWorkspaceCustomUnitAndRateParams = { policyID: string; - lastModified?: number | string; + lastModified?: string; customUnit: string; customUnitRate: string; }; diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx index 1f5d7d854046..6011c303f535 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx @@ -56,7 +56,7 @@ function WorkspaceRateAndUnitPage({policy, route}: WorkspaceRateAndUnitPageProps const currentCustomUnitRate = Object.values(distanceCustomUnit?.rates ?? {}).find((r) => r.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); const unitID = distanceCustomUnit.customUnitID ?? ''; const unitName = distanceCustomUnit.name ?? ''; - const rateNumValue = PolicyUtils.getNumericValue(rate, toLocaleDigit) as number; + const rateNumValue = PolicyUtils.getNumericValue(rate, toLocaleDigit); const newCustomUnit: Policy.NewCustomUnit = { customUnitID: unitID, @@ -64,7 +64,7 @@ function WorkspaceRateAndUnitPage({policy, route}: WorkspaceRateAndUnitPageProps attributes: {unit}, rates: { ...currentCustomUnitRate, - rate: rateNumValue * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, + rate: Number(rateNumValue) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, }, }; diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx b/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx index a61313b8b1e3..cb0b5bce7fc3 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.tsx @@ -127,19 +127,6 @@ function WorkspaceReimburseView({policy, reimbursementAccount}: WorkspaceReimbur /> - - Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(policy?.id ?? ''))} - wrapperStyle={[styles.mhn5, styles.wAuto]} - brickRoadIndicator={(distanceCustomUnit?.errors ?? distanceCustomRate?.errors) && CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR} - /> - Date: Fri, 9 Feb 2024 20:05:31 +0530 Subject: [PATCH 0163/1113] fix: lint --- src/pages/EnablePayments/OnfidoPrivacy.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/OnfidoPrivacy.tsx b/src/pages/EnablePayments/OnfidoPrivacy.tsx index 8618499397dc..03f8df3ad82a 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.tsx +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -46,8 +46,8 @@ function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPr BankAccounts.openOnfidoFlow(); }; - const onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; - const onfidoFixableErrors = walletOnfidoData?.fixableErrors??[]; + const onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) ?? ''; + const onfidoFixableErrors = walletOnfidoData?.fixableErrors ?? []; if (Array.isArray(onfidoError)) { onfidoError[0] += !isEmptyObject(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; } From e62376735e1355fa7701fd8608afcb7cf0461d28 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Fri, 9 Feb 2024 21:53:06 +0530 Subject: [PATCH 0164/1113] fix: action item taking extra space --- src/pages/home/report/ReportActionItem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f960b987427b..d96ae8e77872 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -627,7 +627,7 @@ function ReportActionItem(props) { if (ReportUtils.isTaskReport(props.report)) { if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { return ( - + + Date: Fri, 9 Feb 2024 21:57:04 +0530 Subject: [PATCH 0165/1113] fix: clean lint --- src/pages/home/report/ReportActionItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d96ae8e77872..57f716adc4cd 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -627,7 +627,7 @@ function ReportActionItem(props) { if (ReportUtils.isTaskReport(props.report)) { if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { return ( - + Date: Sat, 10 Feb 2024 13:11:13 +0530 Subject: [PATCH 0166/1113] updated key from ADDITIONAL_DETAILS_FORM to WALLET_ADDITIONAL_DETAILS --- src/ONYXKEYS.ts | 8 ++++---- src/libs/actions/Wallet.ts | 9 +++++---- src/pages/EnablePayments/AdditionalDetailsStep.tsx | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 90a6860fb1de..b14f0985bfed 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -369,8 +369,8 @@ const ONYXKEYS = { PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', - ADDITIONAL_DETAILS_FORM: 'additionalDetailStep', - ADDITIONAL_DETAILS_FORM_DRAFT: 'additionalDetailStepDraft', + WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetailsForm', + WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsFormDraft', }, } as const; @@ -562,8 +562,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_DRAFT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: OnyxTypes.IdologyQuestionsForm; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM_DRAFT]: OnyxTypes.IdologyQuestionsForm; - [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM]: OnyxTypes.AdditionalDetailStepForm; - [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM_DRAFT]: OnyxTypes.AdditionalDetailStepForm; + [ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.AdditionalDetailStepForm; + [ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT]: OnyxTypes.AdditionalDetailStepForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index b03b5e8f6d3d..4d0ba7a67fd1 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -75,11 +75,12 @@ function setKYCWallSource(source?: ValueOf, chatRe /** * Validates a user's provided details against a series of checks */ + function updatePersonalDetails(personalDetails: UpdatePersonalDetailsForWalletParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: true, errors: null, @@ -91,7 +92,7 @@ function updatePersonalDetails(personalDetails: UpdatePersonalDetailsForWalletPa const finallyData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: false, }, @@ -232,7 +233,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: true, }, @@ -242,7 +243,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const finallyData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: false, }, diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index ad802a74c050..c9b557a197e8 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -60,7 +60,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO const maxDate = subYears(currentDate, CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); const shouldAskForFullSSN = walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN; - const validate = (values: OnyxFormValuesFields) => { + const validate = (values: OnyxFormValuesFields) => { const requiredFields = ['legalFirstName', 'legalLastName', 'addressStreet', 'addressCity', 'addressZipCode', 'phoneNumber', 'dob', 'ssn', 'addressState']; const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); @@ -97,7 +97,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO return errors; }; - const activateWallet = (values: OnyxFormValuesFields) => { + const activateWallet = (values: OnyxFormValuesFields) => { const personalDetails = { phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', legalFirstName: values.legalFirstName ?? '', @@ -147,7 +147,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO Date: Mon, 12 Feb 2024 05:45:08 +0530 Subject: [PATCH 0167/1113] fix: background image horizontal shift on sensor value change --- src/pages/home/report/AnimatedEmptyStateBackground.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/AnimatedEmptyStateBackground.tsx b/src/pages/home/report/AnimatedEmptyStateBackground.tsx index bbc340f5afb0..bcc2275da037 100644 --- a/src/pages/home/report/AnimatedEmptyStateBackground.tsx +++ b/src/pages/home/report/AnimatedEmptyStateBackground.tsx @@ -12,7 +12,7 @@ function AnimatedEmptyStateBackground() { const StyleUtils = useStyleUtils(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const illustrations = useThemeIllustrations(); - const IMAGE_OFFSET_X = 0; + const IMAGE_OFFSET_X = windowWidth * 1.1; // If window width is greater than the max background width, repeat the background image const maxBackgroundWidth = variables.sideBarWidth + CONST.EMPTY_STATE_BACKGROUND.ASPECT_RATIO * CONST.EMPTY_STATE_BACKGROUND.WIDE_SCREEN.IMAGE_HEIGHT; @@ -37,7 +37,7 @@ function AnimatedEmptyStateBackground() { xOffset.value = clamp(xOffset.value + y * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_X, IMAGE_OFFSET_X); yOffset.value = clamp(yOffset.value - x * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_Y, IMAGE_OFFSET_Y); return { - transform: [{translateX: withSpring(-IMAGE_OFFSET_X - xOffset.value)}, {translateY: withSpring(yOffset.value)}], + transform: [{translateX: withSpring(xOffset.value)}, {translateY: withSpring(yOffset.value)}, {scale: 1.15}], }; }, [isReducedMotionEnabled]); From 977512b09fa0d41ebe06623e6394320ef942b349 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Mon, 12 Feb 2024 07:30:55 +0530 Subject: [PATCH 0168/1113] refactor: move image offset values outside of component --- src/pages/home/report/AnimatedEmptyStateBackground.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/AnimatedEmptyStateBackground.tsx b/src/pages/home/report/AnimatedEmptyStateBackground.tsx index bcc2275da037..3a920f4f8449 100644 --- a/src/pages/home/report/AnimatedEmptyStateBackground.tsx +++ b/src/pages/home/report/AnimatedEmptyStateBackground.tsx @@ -6,13 +6,14 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -const IMAGE_OFFSET_Y = 75; +// Maximum horizontal and vertical shift in pixels on sensor value change +const IMAGE_OFFSET_X = 30; +const IMAGE_OFFSET_Y = 20; function AnimatedEmptyStateBackground() { const StyleUtils = useStyleUtils(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const illustrations = useThemeIllustrations(); - const IMAGE_OFFSET_X = windowWidth * 1.1; // If window width is greater than the max background width, repeat the background image const maxBackgroundWidth = variables.sideBarWidth + CONST.EMPTY_STATE_BACKGROUND.ASPECT_RATIO * CONST.EMPTY_STATE_BACKGROUND.WIDE_SCREEN.IMAGE_HEIGHT; @@ -37,6 +38,8 @@ function AnimatedEmptyStateBackground() { xOffset.value = clamp(xOffset.value + y * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_X, IMAGE_OFFSET_X); yOffset.value = clamp(yOffset.value - x * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_Y, IMAGE_OFFSET_Y); return { + // On Android, scroll view sub views gets clipped beyond container bounds. Set the top position so that image wouldn't get clipped + top: IMAGE_OFFSET_Y, transform: [{translateX: withSpring(xOffset.value)}, {translateY: withSpring(yOffset.value)}, {scale: 1.15}], }; }, [isReducedMotionEnabled]); From 01bcba22a5abeb0eb5b541109e340db64e50ca13 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 12 Feb 2024 12:58:28 +0530 Subject: [PATCH 0169/1113] rearranged import for GetPhysicalCardForm --- src/types/onyx/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index ca3abbca28b9..c9742595272f 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -16,8 +16,8 @@ import type { CloseAccountForm, DateOfBirthForm, DisplayNameForm, - IdologyQuestionsForm, GetPhysicalCardForm, + IdologyQuestionsForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, From ee76e50335a44ed6053292ee8deb78423a3edacf Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 12 Feb 2024 20:17:11 +0530 Subject: [PATCH 0170/1113] updated IDOLOGY_QUESTIONS_FORM key name --- src/ONYXKEYS.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f19adfbeedd3..29a2955facfa 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -367,8 +367,8 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm', PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', - IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', - IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', + IDOLOGY_QUESTIONS_FORM: 'idologyQuestionsForm', + IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsFormDraft', WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetailsForm', WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsFormDraft', }, From 26e354f83e6c3fc8c7281d12d911ecd8c24ff0d4 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Mon, 12 Feb 2024 09:12:06 -0800 Subject: [PATCH 0171/1113] Fix formatting error --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e329d6e51a49..d86c838ccccd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2308,7 +2308,6 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } - const lastActorID = reportAction?.actorAccountID; const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; let amount = originalMessage?.amount; From 55fceb1ad731bd95461db946f13c342f860ff233 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 12 Feb 2024 21:19:20 +0100 Subject: [PATCH 0172/1113] update config --- metro.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metro.config.js b/metro.config.js index 2422d29aaacf..68ed72d52ba0 100644 --- a/metro.config.js +++ b/metro.config.js @@ -7,7 +7,7 @@ require('dotenv').config(); const defaultConfig = getDefaultConfig(__dirname); const isE2ETesting = process.env.E2E_TESTING === 'true'; -const e2eSourceExts = ['e2e.js', 'e2e.ts']; +const e2eSourceExts = ['e2e.js', 'e2e.ts', 'e2e.tsx']; /** * Metro configuration From 82a1aaadb2f29a4a2a6a6386cd6ad466fad651d7 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 13 Feb 2024 09:23:21 +0100 Subject: [PATCH 0173/1113] add onViewableItemsChanged --- .../BaseInvertedFlatList/index.e2e.tsx | 50 +++++++++++++++++++ .../index.tsx} | 1 + 2 files changed, 51 insertions(+) create mode 100644 src/components/InvertedFlatList/BaseInvertedFlatList/index.e2e.tsx rename src/components/InvertedFlatList/{BaseInvertedFlatList.tsx => BaseInvertedFlatList/index.tsx} (96%) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.e2e.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.e2e.tsx new file mode 100644 index 000000000000..0553312eae32 --- /dev/null +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.e2e.tsx @@ -0,0 +1,50 @@ +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useMemo} from 'react'; +import type {FlatListProps, ScrollViewProps} from 'react-native'; +import FlatList from '@components/FlatList'; + +type BaseInvertedFlatListProps = FlatListProps & { + shouldEnableAutoScrollToTopThreshold?: boolean; +}; + +const AUTOSCROLL_TO_TOP_THRESHOLD = 128; + +let localViewableItems: unknown; +const getViewableItems = () => localViewableItems; + +function BaseInvertedFlatListE2e(props: BaseInvertedFlatListProps, ref: ForwardedRef) { + const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; + + const handleViewableItemsChanged = ({viewableItems}: { viewableItems: unknown }) => { + localViewableItems = viewableItems; + }; + + const maintainVisibleContentPosition = useMemo(() => { + const config: ScrollViewProps['maintainVisibleContentPosition'] = { + // This needs to be 1 to avoid using loading views as anchors. + minIndexForVisible: 1, + }; + + if (shouldEnableAutoScrollToTopThreshold) { + config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; + } + + return config; + }, [shouldEnableAutoScrollToTopThreshold]); + + return ( + + ); +} + +BaseInvertedFlatListE2e.displayName = 'BaseInvertedFlatListE2e'; + +export default forwardRef(BaseInvertedFlatListE2e); +export {getViewableItems}; diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx similarity index 96% rename from src/components/InvertedFlatList/BaseInvertedFlatList.tsx rename to src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index d83e54f74d66..c92203afdc74 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -25,6 +25,7 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa return config; }, [shouldEnableAutoScrollToTopThreshold]); + console.debug(`[E2E] BaseInverted.NOT`); return ( Date: Tue, 13 Feb 2024 09:28:41 +0100 Subject: [PATCH 0174/1113] linking test --- src/ROUTES.ts | 4 ++ src/libs/E2E/reactNativeLaunchingTest.ts | 1 + src/libs/E2E/tests/linkingTest.e2e.ts | 69 ++++++++++++++++++++++ src/pages/home/report/ReportActionsView.js | 4 +- tests/e2e/config.js | 11 ++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/libs/E2E/tests/linkingTest.e2e.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5a2ab8cfc7de..996136d12e6d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -169,6 +169,10 @@ const ROUTES = { route: 'r/:reportID/avatar', getRoute: (reportID: string) => `r/${reportID}/avatar` as const, }, + REPORT_WITH_ID_AND_ACTION_ID: { + route: 'r/:reportID?/:reportActionID?', + getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/${reportActionID}` as const, + }, EDIT_REQUEST: { route: 'r/:threadReportID/edit/:field', getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}` as const, diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index 79276e7a5d75..931b41524696 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -38,6 +38,7 @@ const tests: Tests = { [E2EConfig.TEST_NAMES.OpenSearchPage]: require('./tests/openSearchPageTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, + [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, }; // Once we receive the TII measurement we know that the app is initialized and ready to be used: diff --git a/src/libs/E2E/tests/linkingTest.e2e.ts b/src/libs/E2E/tests/linkingTest.e2e.ts new file mode 100644 index 000000000000..ef370aaecf17 --- /dev/null +++ b/src/libs/E2E/tests/linkingTest.e2e.ts @@ -0,0 +1,69 @@ +import Config from 'react-native-config'; +import {getViewableItems} from '@components/InvertedFlatList/BaseInvertedFlatList/index.e2e'; +import Timing from '@libs/actions/Timing'; +import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; +import E2EClient from '@libs/E2E/client'; +import type {TestConfig} from '@libs/E2E/types'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; +import Navigation from '@libs/Navigation/Navigation'; +import Performance from '@libs/Performance'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +const test = (config: TestConfig) => { + console.debug('[E2E] Logging in for comment linking'); + + const reportID = getConfigValueOrThrow('reportID', config); + const linkedReportID = getConfigValueOrThrow('linkedReportID', config); + const linkedReportActionID = getConfigValueOrThrow('linkedReportActionID', config); + + E2ELogin().then((neededLogin) => { + if (neededLogin) { + return waitForAppLoaded().then(() => E2EClient.submitTestDone()); + } + + Performance.subscribeToMeasurements((entry) => { + if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { + console.debug('[E2E] Sidebar loaded, navigating to a report…'); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + return; + } + + if (entry.name === CONST.TIMING.REPORT_INITIAL_RENDER) { + console.debug('[E2E] Navigating to linked report action…'); + Timing.start(CONST.TIMING.SWITCH_REPORT); + Performance.markStart(CONST.TIMING.SWITCH_REPORT); + + Navigation.navigate(ROUTES.REPORT_WITH_ID_AND_ACTION_ID.getRoute(linkedReportID, linkedReportActionID)); + return; + } + + if (entry.name === CONST.TIMING.SWITCH_REPORT) { + setTimeout(() => { + const res = getViewableItems(); + console.debug('[E2E] Viewable items retrieved, verifying correct message…'); + + if (res[0]?.item?.reportActionID === linkedReportActionID) { + E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, + name: 'Comment linking', + duration: entry.duration, + }) + .then(() => { + console.debug('[E2E] Test completed successfully, exiting…'); + E2EClient.submitTestDone(); + }) + .catch((err) => { + console.debug('[E2E] Error while submitting test results:', err); + }); + } else { + console.debug('[E2E] Message verification failed'); + } + }, 3000); + } + }); + }); +}; + +export default test; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 56f204ef6ffb..bcec51dba4e1 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -384,15 +384,15 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { } 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); + Timing.end(CONST.TIMING.REPORT_INITIAL_RENDER); ReportActionsView.initMeasured = true; } else { Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); }, [hasCachedActions], ); diff --git a/tests/e2e/config.js b/tests/e2e/config.js index a7447a29c954..79d4c7c4bdfa 100644 --- a/tests/e2e/config.js +++ b/tests/e2e/config.js @@ -6,6 +6,7 @@ const TEST_NAMES = { OpenSearchPage: 'Open search page TTI', ReportTyping: 'Report typing', ChatOpening: 'Chat opening', + Linking: 'Linking', }; /** @@ -85,5 +86,15 @@ module.exports = { // #announce Chat with many messages reportID: '5421294415618529', }, + [TEST_NAMES.Linking]: { + name: TEST_NAMES.Linking, + reportScreen: { + autoFocus: true, + }, + // Crowded Policy (Do Not Delete) Report, has a input bar available: + reportID: '8268282951170052', + linkedReportID: '5421294415618529', + linkedReportActionID: '2845024374735019929', + }, }, }; From bb4ce473435efb3e5def3c75b07dea516aeaeea3 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 15 Feb 2024 04:37:42 +0500 Subject: [PATCH 0175/1113] add policyReportFields to the policy object directly --- src/ONYXKEYS.ts | 2 -- .../ReportActionItem/MoneyReportView.tsx | 9 ++--- src/libs/ReportUtils.ts | 27 ++------------- src/pages/EditReportFieldPage.tsx | 12 ++----- src/pages/home/report/ReportActionItem.js | 6 ---- src/types/onyx/Policy.ts | 33 ++++++++++++++++++- src/types/onyx/PolicyReportField.ts | 30 ----------------- src/types/onyx/index.ts | 3 +- 8 files changed, 42 insertions(+), 80 deletions(-) delete mode 100644 src/types/onyx/PolicyReportField.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5755296f3bb5..07061ab0bfc0 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -278,7 +278,6 @@ const ONYXKEYS = { POLICY_TAGS: 'policyTags_', POLICY_TAX_RATE: 'policyTaxRates_', POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', - POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', REPORT: 'report_', @@ -439,7 +438,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index f0cd8dc1b4b5..bf1980578079 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -28,14 +28,11 @@ type MoneyReportViewProps = { /** Policy that the report belongs to */ policy: Policy; - /** Policy report fields */ - policyReportFields: PolicyReportField[]; - /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; }; -function MoneyReportView({report, policy, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) { +function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReportViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -59,9 +56,9 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont ]; const sortedPolicyReportFields = useMemo((): PolicyReportField[] => { - const fields = ReportUtils.getAvailableReportFields(report, policyReportFields); + const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy.reportFields || {})); return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); - }, [policyReportFields, report]); + }, [policy, report]); return ( diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ebde1b1bf8ab..e8066e37467f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -15,20 +15,7 @@ import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languag import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type { - Beta, - PersonalDetails, - PersonalDetailsList, - Policy, - PolicyReportField, - PolicyReportFields, - Report, - ReportAction, - ReportMetadata, - Session, - Transaction, - TransactionViolation, -} from '@src/types/onyx'; +import type {Beta, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type { @@ -463,14 +450,6 @@ Onyx.connect({ callback: (value) => (allPolicies = value), }); -let allPolicyReportFields: OnyxCollection = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS, - waitForCollectionCallback: true, - callback: (value) => (allPolicyReportFields = value), -}); - let allBetas: OnyxEntry; Onyx.connect({ key: ONYXKEYS.BETAS, @@ -1972,7 +1951,7 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry /** * Given a set of report fields, return the field of type formula */ -function getFormulaTypeReportField(reportFields: PolicyReportFields) { +function getFormulaTypeReportField(reportFields: Record) { return Object.values(reportFields).find((field) => field.type === 'formula'); } @@ -1980,7 +1959,7 @@ function getFormulaTypeReportField(reportFields: PolicyReportFields) { * Get the report fields attached to the policy given policyID */ function getReportFieldsByPolicyID(policyID: string) { - return Object.entries(allPolicyReportFields ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS, '') === policyID)?.[1]; + return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.reportFields; } /** diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 4124a9ebef98..015b2cabd51c 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -9,7 +9,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActions from '@src/libs/actions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyReportFields, Report} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import EditReportFieldDatePage from './EditReportFieldDatePage'; import EditReportFieldDropdownPage from './EditReportFieldDropdownPage'; import EditReportFieldTextPage from './EditReportFieldTextPage'; @@ -18,9 +18,6 @@ type EditReportFieldPageOnyxProps = { /** The report object for the expense report */ report: OnyxEntry; - /** Policy report fields */ - policyReportFields: OnyxEntry; - /** Policy to which the report belongs to */ policy: OnyxEntry; }; @@ -42,8 +39,8 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; }; -function EditReportFieldPage({route, policy, report, policyReportFields}: EditReportFieldPageProps) { - const reportField = report?.reportFields?.[route.params.fieldID] ?? policyReportFields?.[route.params.fieldID]; +function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { + const reportField = report?.reportFields?.[route.params.fieldID] ?? policy?.reportFields?.[route.params.fieldID]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); if (!reportField || !report || isDisabled) { @@ -121,9 +118,6 @@ export default withOnyx( report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, }, - policyReportFields: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${route.params.policyID}`, - }, policy: { key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, }, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 39a5fcaa4ee0..4281adeb3eaa 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -672,7 +672,6 @@ function ReportActionItem(props) { @@ -836,10 +835,6 @@ export default compose( }, initialValue: {}, }, - policyReportFields: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), - initialValue: [], - }, policy: { key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined), initialValue: {}, @@ -886,7 +881,6 @@ export default compose( lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) && _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) && _.isEqual(prevProps.policy, nextProps.policy), ), diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 7d4c08374b81..46d07a56183c 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -45,6 +45,34 @@ type Connection = { type AutoReportingOffset = number | ValueOf; +type PolicyReportFieldType = 'text' | 'date' | 'dropdown' | 'formula'; + +type PolicyReportField = { + /** Name of the field */ + name: string; + + /** Default value assigned to the field */ + defaultValue: string; + + /** Unique id of the field */ + fieldID: string; + + /** Position at which the field should show up relative to the other fields */ + orderWeight: number; + + /** Type of report field */ + type: PolicyReportFieldType; + + /** Tells if the field is required or not */ + deletable: boolean; + + /** Value of the field */ + value: string; + + /** Options to select from if field is of type dropdown */ + values: string[]; +}; + type Policy = { /** The ID of the policy */ id: string; @@ -179,8 +207,11 @@ type Policy = { /** All the integration connections attached to the policy */ connections?: Record; + + /** Report fields attached to the policy */ + reportFields?: Record; }; export default Policy; -export type {Unit, CustomUnit, Attributes, Rate}; +export type {Unit, CustomUnit, Attributes, Rate, PolicyReportField, PolicyReportFieldType}; diff --git a/src/types/onyx/PolicyReportField.ts b/src/types/onyx/PolicyReportField.ts deleted file mode 100644 index de385070aa25..000000000000 --- a/src/types/onyx/PolicyReportField.ts +++ /dev/null @@ -1,30 +0,0 @@ -type PolicyReportFieldType = 'text' | 'date' | 'dropdown' | 'formula'; - -type PolicyReportField = { - /** Name of the field */ - name: string; - - /** Default value assigned to the field */ - defaultValue: string; - - /** Unique id of the field */ - fieldID: string; - - /** Position at which the field should show up relative to the other fields */ - orderWeight: number; - - /** Type of report field */ - type: PolicyReportFieldType; - - /** Tells if the field is required or not */ - deletable: boolean; - - /** Value of the field */ - value: string; - - /** Options to select from if field is of type dropdown */ - values: string[]; -}; - -type PolicyReportFields = Record; -export type {PolicyReportField, PolicyReportFields}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 1b2ecdbdce12..e87a54ab6623 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -30,10 +30,10 @@ import type {PersonalDetailsList} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; import type Policy from './Policy'; +import type {PolicyReportField} from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; -import type {PolicyReportField, PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; @@ -143,7 +143,6 @@ export type { WorkspaceRateAndUnit, ReportUserIsTyping, PolicyReportField, - PolicyReportFields, RecentlyUsedReportFields, LastPaymentMethod, InvitedEmailsToAccountIDs, From b84b0bf483e2db39fa1d290182ffdb06285fd9a7 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 15 Feb 2024 04:47:04 +0500 Subject: [PATCH 0176/1113] fix: type errors --- src/types/onyx/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index fbd61a9c5365..4a8b41ca4c5b 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -2,7 +2,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type * as OnyxCommon from './OnyxCommon'; import type PersonalDetails from './PersonalDetails'; -import type {PolicyReportField} from './PolicyReportField'; +import type {PolicyReportField} from './Policy'; type NotificationPreference = ValueOf; From a9315c4e0158438aa4bc8a2935c707828e7c2cb6 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 15 Feb 2024 16:42:37 +0100 Subject: [PATCH 0177/1113] migrate AttachmentView --- src/CONST.ts | 4 +- .../{index.js => index.tsx} | 30 +++--- .../AttachmentViewImage/propTypes.js | 21 ---- ...ntViewPdf.js => BaseAttachmentViewPdf.tsx} | 32 +++---- .../{index.android.js => index.android.tsx} | 10 +- .../{index.ios.js => index.ios.tsx} | 7 +- .../AttachmentViewPdf/{index.js => index.tsx} | 8 +- .../AttachmentViewPdf/propTypes.js | 28 ------ .../AttachmentView/AttachmentViewPdf/types.ts | 21 ++++ .../AttachmentView/{index.js => index.tsx} | 95 ++++++++----------- .../Attachments/AttachmentView/propTypes.js | 52 ---------- .../Attachments/AttachmentView/types.ts | 38 ++++++++ src/components/Attachments/propTypes.js | 21 ---- src/components/Attachments/types.ts | 32 +++++++ src/components/ImageView/types.ts | 2 +- 15 files changed, 171 insertions(+), 230 deletions(-) rename src/components/Attachments/AttachmentView/AttachmentViewImage/{index.js => index.tsx} (67%) mode change 100755 => 100644 delete mode 100644 src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{BaseAttachmentViewPdf.js => BaseAttachmentViewPdf.tsx} (83%) rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{index.android.js => index.android.tsx} (94%) rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{index.ios.js => index.ios.tsx} (54%) rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{index.js => index.tsx} (73%) delete mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts rename src/components/Attachments/AttachmentView/{index.js => index.tsx} (76%) mode change 100755 => 100644 delete mode 100644 src/components/Attachments/AttachmentView/propTypes.js create mode 100644 src/components/Attachments/AttachmentView/types.ts delete mode 100644 src/components/Attachments/propTypes.js create mode 100644 src/components/Attachments/types.ts diff --git a/src/CONST.ts b/src/CONST.ts index 5c99c5877559..fbcabdd64014 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -437,7 +437,7 @@ const CONST = { }, }, ARROW_LEFT: { - descriptionKey: null, + descriptionKey: 'arrowLeft', shortcutKey: 'ArrowLeft', modifiers: [], trigger: { @@ -447,7 +447,7 @@ const CONST = { }, }, ARROW_RIGHT: { - descriptionKey: null, + descriptionKey: 'arrowRight', shortcutKey: 'ArrowRight', modifiers: [], trigger: { diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx old mode 100755 new mode 100644 similarity index 67% rename from src/components/Attachments/AttachmentView/AttachmentViewImage/index.js rename to src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index 14c60458b044..9a28dfd82bc4 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -1,16 +1,18 @@ import React, {memo} from 'react'; +import type AttachmentViewBaseProps from '@components/Attachments/AttachmentView/types'; import ImageView from '@components/ImageView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import CONST from '@src/CONST'; -import {attachmentViewImageDefaultProps, attachmentViewImagePropTypes} from './propTypes'; -const propTypes = { - ...attachmentViewImagePropTypes, - ...withLocalizePropTypes, -}; +type AttachmentViewImageProps = { + url: string | number; + + loadComplete: boolean; + + isImage: boolean; +} & AttachmentViewBaseProps; function AttachmentViewImage({ url, @@ -20,21 +22,19 @@ function AttachmentViewImage({ isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex, - isFocused, loadComplete, onPress, onError, isImage, - translate, -}) { +}: AttachmentViewImageProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); const children = ( {children} @@ -57,8 +57,6 @@ function AttachmentViewImage({ ); } -AttachmentViewImage.propTypes = propTypes; -AttachmentViewImage.defaultProps = attachmentViewImageDefaultProps; AttachmentViewImage.displayName = 'AttachmentViewImage'; -export default compose(memo, withLocalize)(AttachmentViewImage); +export default memo(AttachmentViewImage); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js deleted file mode 100644 index f2a275fc9a21..000000000000 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; -import {attachmentViewDefaultProps, attachmentViewPropTypes} from '@components/Attachments/AttachmentView/propTypes'; - -const attachmentViewImagePropTypes = { - ...attachmentViewPropTypes, - - url: PropTypes.string.isRequired, - - loadComplete: PropTypes.bool.isRequired, - - isImage: PropTypes.bool.isRequired, -}; - -const attachmentViewImageDefaultProps = { - ...attachmentViewDefaultProps, - - loadComplete: false, - isImage: false, -}; - -export {attachmentViewImagePropTypes, attachmentViewImageDefaultProps}; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx similarity index 83% rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx index 2f16b63aacc6..213a28d830e7 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx @@ -1,21 +1,13 @@ -import PropTypes from 'prop-types'; import React, {memo, useCallback, useContext, useEffect} from 'react'; +import type {GestureResponderEvent} from 'react-native'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import PDFView from '@components/PDFView'; -import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; - -const baseAttachmentViewPdfPropTypes = { - ...attachmentViewPdfPropTypes, +import type AttachmentViewPdfProps from './types'; +type BaseAttachmentViewPdfProps = { /** Triggered when the PDF's onScaleChanged event is triggered */ - onScaleChanged: PropTypes.func, -}; - -const baseAttachmentViewPdfDefaultProps = { - ...attachmentViewPdfDefaultProps, - - onScaleChanged: undefined, -}; + onScaleChanged: (scale: number) => void; +} & AttachmentViewPdfProps; function BaseAttachmentViewPdf({ file, @@ -28,7 +20,7 @@ function BaseAttachmentViewPdf({ onLoadComplete, errorLabelStyles, style, -}) { +}: BaseAttachmentViewPdfProps) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; @@ -46,7 +38,7 @@ function BaseAttachmentViewPdf({ * as well as call the onScaleChanged prop of the AttachmentViewPdf component if defined. */ const onScaleChanged = useCallback( - (newScale) => { + (newScale: number) => { if (onScaleChangedProp !== undefined) { onScaleChangedProp(newScale); } @@ -66,13 +58,13 @@ function BaseAttachmentViewPdf({ * Otherwise it means that the PDF is currently zoomed in, therefore the onTap callback should be ignored */ const onPress = useCallback( - (e) => { + (e?: GestureResponderEvent | KeyboardEvent) => { if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && isScrollEnabled.value) { - attachmentCarouselPagerContext.onTap(e); + if (attachmentCarouselPagerContext !== null && isScrollEnabled?.value) { + attachmentCarouselPagerContext.onTap(); } }, [attachmentCarouselPagerContext, isScrollEnabled, onPressProp], @@ -80,6 +72,7 @@ function BaseAttachmentViewPdf({ return ( { isPanGestureActive.value = false; + if (!isScrollEnabled) { + return; + } isScrollEnabled.value = true; }); @@ -93,7 +96,4 @@ function AttachmentViewPdf(props) { ); } -AttachmentViewPdf.propTypes = attachmentViewPdfPropTypes; -AttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps; - export default memo(AttachmentViewPdf); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx similarity index 54% rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx index 103ff292760f..79c9974bc8ce 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx @@ -1,8 +1,8 @@ import React, {memo} from 'react'; +import type {BaseAttachmentViewPdfProps} from './BaseAttachmentViewPdf'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; -import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf(props) { +function AttachmentViewPdf(props: BaseAttachmentViewPdfProps) { return ( void; + onLoadComplete: () => void; + + /** Additional style props */ + style?: StyleProp; + + /** Styles for the error label */ + errorLabelStyles?: StyleProp; + + /** Whether this view is the active screen */ + isFocused?: boolean; +} & AttachmentViewBaseProps & + Attachment; + +export default AttachmentViewPdfProps; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.tsx old mode 100755 new mode 100644 similarity index 76% rename from src/components/Attachments/AttachmentView/index.js rename to src/components/Attachments/AttachmentView/index.tsx index 33eab13f3851..033515621092 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,82 +1,74 @@ import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; import React, {memo, useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, ScrollView, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; +import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; -import compose from '@libs/compose'; import * as TransactionUtils from '@libs/TransactionUtils'; +import type {ColorValue} from '@styles/utils/types'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Transaction} from '@src/types/onyx'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; -import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes'; +import type AttachmentViewBaseProps from './types'; -const propTypes = { - ...attachmentViewPropTypes, - ...withLocalizePropTypes, +type AttachmentViewOnyxProps = { + transaction: OnyxEntry; +}; +type AttachmentViewProps = { /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, + source: AttachmentSource; /** Flag to show/hide download icon */ - shouldShowDownloadIcon: PropTypes.bool, + shouldShowDownloadIcon?: boolean; /** Flag to show the loading indicator */ - shouldShowLoadingSpinnerIcon: PropTypes.bool, + shouldShowLoadingSpinnerIcon?: boolean; /** Notify parent that the UI should be modified to accommodate keyboard */ - onToggleKeyboard: PropTypes.func, + onToggleKeyboard?: () => void; /** Extra styles to pass to View wrapper */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), + containerStyles?: Array>; /** Denotes whether it is a workspace avatar or not */ - isWorkspaceAvatar: PropTypes.bool, + isWorkspaceAvatar?: boolean; /** Denotes whether it is an icon (ex: SVG) */ - maybeIcon: PropTypes.bool, + maybeIcon?: boolean; /** The id of the transaction related to the attachment */ - // eslint-disable-next-line react/no-unused-prop-types - transactionID: PropTypes.string, -}; + transactionID?: string; -const defaultProps = { - ...attachmentViewDefaultProps, - shouldShowDownloadIcon: false, - shouldShowLoadingSpinnerIcon: false, - onToggleKeyboard: () => {}, - containerStyles: [], - isWorkspaceAvatar: false, - maybeIcon: false, - transactionID: '', -}; + fallbackSource?: string | number; +} & AttachmentViewOnyxProps & + AttachmentViewBaseProps & + Attachment; function AttachmentView({ source, - file, + file = {name: ''}, isAuthTokenRequired, onPress, shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, onToggleKeyboard, - translate, isFocused, isUsedInCarousel, isSingleCarouselItem, @@ -87,7 +79,8 @@ function AttachmentView({ maybeIcon, fallbackSource, transaction, -}) { +}: AttachmentViewProps) { + const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -98,10 +91,10 @@ function AttachmentView({ // Handles case where source is a component (ex: SVG) or a number // Number may represent a SVG or an image - if ((maybeIcon && typeof source === 'number') || _.isFunction(source)) { - let iconFillColor = ''; - let additionalStyles = []; - if (isWorkspaceAvatar) { + if ((maybeIcon && typeof source === 'number') ?? typeof source === 'function') { + let iconFillColor: ColorValue | undefined = ''; + let additionalStyles: ViewStyle[] = []; + if (isWorkspaceAvatar && file) { const defaultWorkspaceAvatarColor = StyleUtils.getDefaultWorkspaceAvatarColor(file.name); iconFillColor = defaultWorkspaceAvatarColor.fill; additionalStyles = [defaultWorkspaceAvatarColor]; @@ -118,7 +111,7 @@ function AttachmentView({ ); } - if (TransactionUtils.hasEReceipt(transaction)) { + if (TransactionUtils.hasEReceipt(transaction) && transaction) { return ( + - {file && file.name} + {file?.name} {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( @@ -223,16 +216,10 @@ function AttachmentView({ ); } -AttachmentView.propTypes = propTypes; -AttachmentView.defaultProps = defaultProps; AttachmentView.displayName = 'AttachmentView'; -export default compose( - memo, - withLocalize, - withOnyx({ - transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - }, - }), -)(AttachmentView); +export default withOnyx({ + transaction: { + key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + }, +})(memo(AttachmentView)); diff --git a/src/components/Attachments/AttachmentView/propTypes.js b/src/components/Attachments/AttachmentView/propTypes.js deleted file mode 100644 index d78bed8526b8..000000000000 --- a/src/components/Attachments/AttachmentView/propTypes.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; - -const attachmentViewPropTypes = { - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** File object can be an instance of File or Object */ - file: AttachmentsPropTypes.attachmentFilePropType, - - /** Whether this view is the active screen */ - isFocused: PropTypes.bool, - - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ - isUsedInCarousel: PropTypes.bool, - - /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ - isSingleCarouselItem: PropTypes.bool, - - /** Whether this AttachmentView is shown as part of an AttachmentModal */ - isUsedInAttachmentModal: PropTypes.bool, - - /** The index of the carousel item */ - carouselItemIndex: PropTypes.number, - - /** The index of the currently active carousel item */ - carouselActiveItemIndex: PropTypes.number, - - /** Function for handle on press */ - onPress: PropTypes.func, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, -}; - -const attachmentViewDefaultProps = { - isAuthTokenRequired: false, - file: { - name: '', - }, - isFocused: false, - isUsedInCarousel: false, - isSingleCarouselItem: false, - carouselItemIndex: 0, - carouselActiveItemIndex: 0, - isSingleElement: false, - isUsedInAttachmentModal: false, - onPress: undefined, - onScaleChanged: () => {}, -}; - -export {attachmentViewPropTypes, attachmentViewDefaultProps}; diff --git a/src/components/Attachments/AttachmentView/types.ts b/src/components/Attachments/AttachmentView/types.ts new file mode 100644 index 000000000000..21b3a0c01cfd --- /dev/null +++ b/src/components/Attachments/AttachmentView/types.ts @@ -0,0 +1,38 @@ +import type {GestureResponderEvent} from 'react-native'; +import type {AttachmentFile} from '@components/Attachments/types'; + +type AttachmentViewBaseProps = { + /** Whether this view is the active screen */ + isFocused?: boolean; + + /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ + isUsedInCarousel?: boolean; + + /** File object can be an instance of File or Object */ + file: AttachmentFile; + + isAuthTokenRequired?: boolean; + + /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ + isSingleCarouselItem?: boolean; + + /** Whether this AttachmentView is shown as part of an AttachmentModal */ + isUsedInAttachmentModal?: boolean; + + /** The index of the carousel item */ + carouselItemIndex?: number; + + /** The index of the currently active carousel item */ + carouselActiveItemIndex?: number; + + /** Function for handle on press */ + onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void; + + /** Function for handle on error */ + onError?: () => void; + + /** Handles scale changed event */ + onScaleChanged?: (scale: number) => void; +}; + +export default AttachmentViewBaseProps; diff --git a/src/components/Attachments/propTypes.js b/src/components/Attachments/propTypes.js deleted file mode 100644 index 13adc468ce64..000000000000 --- a/src/components/Attachments/propTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; - -const attachmentSourcePropType = PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.number]); -const attachmentFilePropType = PropTypes.shape({ - name: PropTypes.string.isRequired, -}); - -const attachmentPropType = PropTypes.shape({ - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: attachmentSourcePropType.isRequired, - - /** File object can be an instance of File or Object */ - file: attachmentFilePropType.isRequired, -}); - -const attachmentsPropType = PropTypes.arrayOf(attachmentPropType); - -export {attachmentSourcePropType, attachmentFilePropType, attachmentPropType, attachmentsPropType}; diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts new file mode 100644 index 000000000000..bb3848e7bbe6 --- /dev/null +++ b/src/components/Attachments/types.ts @@ -0,0 +1,32 @@ +// This can be either a string, function, or number +type AttachmentSource = string | number | React.FC; + +// Object shape for file where name is a required string +type AttachmentFile = { + name: string; +}; + +// The object shape for the attachment +type Attachment = { + /** Report action ID of the attachment */ + reportActionID?: string; + + /** Whether source url requires authentication */ + isAuthTokenRequired?: boolean; + + /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ + source: AttachmentSource; + + /** File object can be an instance of File or Object */ + file: AttachmentFile; + + /** Whether the attachment has been flagged */ + hasBeenFlagged?: boolean; + + /** The id of the transaction related to the attachment */ + transactionID?: string; + + isReceipt?: boolean; +}; + +export type {AttachmentSource, AttachmentFile, Attachment}; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index b85115874a5a..9ff983c3609a 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -6,7 +6,7 @@ type ImageViewProps = { isAuthTokenRequired?: boolean; /** URL to full-sized image */ - url: string; + url: string | number; /** image file name */ fileName: string; From 0f696ce583c5757e6cab8be8f1e34a5c6aa23ab6 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 15 Feb 2024 16:43:29 +0100 Subject: [PATCH 0178/1113] migrate AttachmentCarousel --- ....js => AttachmentCarouselCellRenderer.tsx} | 14 ++--- ...CarouselActions.js => CarouselActions.tsx} | 22 +++----- ...CarouselButtons.js => CarouselButtons.tsx} | 32 +++++------- .../{CarouselItem.js => CarouselItem.tsx} | 51 +++++-------------- ...ort.js => extractAttachmentsFromReport.ts} | 23 ++++----- ...CarouselArrows.js => useCarouselArrows.ts} | 11 ++-- 6 files changed, 51 insertions(+), 102 deletions(-) rename src/components/Attachments/AttachmentCarousel/{AttachmentCarouselCellRenderer.js => AttachmentCarouselCellRenderer.tsx} (71%) rename src/components/Attachments/AttachmentCarousel/{CarouselActions.js => CarouselActions.tsx} (63%) rename src/components/Attachments/AttachmentCarousel/{CarouselButtons.js => CarouselButtons.tsx} (76%) rename src/components/Attachments/AttachmentCarousel/{CarouselItem.js => CarouselItem.tsx} (70%) rename src/components/Attachments/AttachmentCarousel/{extractAttachmentsFromReport.js => extractAttachmentsFromReport.ts} (73%) rename src/components/Attachments/AttachmentCarousel/{useCarouselArrows.js => useCarouselArrows.ts} (77%) diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx similarity index 71% rename from src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js rename to src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx index f4cbffc0e1e4..08d0b7f271d4 100644 --- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js +++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx @@ -1,19 +1,15 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import {PixelRatio, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -const propTypes = { +type AttachmentCarouselCellRendererProps = { /** Cell Container styles */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style: StyleProp; }; -const defaultProps = { - style: [], -}; - -function AttachmentCarouselCellRenderer(props) { +function AttachmentCarouselCellRenderer(props: AttachmentCarouselCellRendererProps) { const styles = useThemeStyles(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true); @@ -28,8 +24,6 @@ function AttachmentCarouselCellRenderer(props) { ); } -AttachmentCarouselCellRenderer.propTypes = propTypes; -AttachmentCarouselCellRenderer.defaultProps = defaultProps; AttachmentCarouselCellRenderer.displayName = 'AttachmentCarouselCellRenderer'; export default React.memo(AttachmentCarouselCellRenderer); diff --git a/src/components/Attachments/AttachmentCarousel/CarouselActions.js b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx similarity index 63% rename from src/components/Attachments/AttachmentCarousel/CarouselActions.js rename to src/components/Attachments/AttachmentCarousel/CarouselActions.tsx index cf5309222c4e..45fed45e1670 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselActions.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx @@ -1,24 +1,19 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import {useEffect} from 'react'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import CONST from '@src/CONST'; -const propTypes = { +type CarouselActionsProps = { /** Callback to cycle through attachments */ - onCycleThroughAttachments: PropTypes.func.isRequired, + onCycleThroughAttachments: (deltaSlide: number) => void; }; -function CarouselActions({onCycleThroughAttachments}) { +function CarouselActions({onCycleThroughAttachments}: CarouselActionsProps) { useEffect(() => { const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT; const unsubscribeLeftKey = KeyboardShortcut.subscribe( shortcutLeftConfig.shortcutKey, - (e) => { - if (lodashGet(e, 'target.blur')) { - // prevents focus from highlighting around the modal - e.target.blur(); - } + (e?: KeyboardEvent) => { + (e as unknown as React.FocusEvent)?.target?.blur(); onCycleThroughAttachments(-1); }, @@ -30,10 +25,7 @@ function CarouselActions({onCycleThroughAttachments}) { const unsubscribeRightKey = KeyboardShortcut.subscribe( shortcutRightConfig.shortcutKey, (e) => { - if (lodashGet(e, 'target.blur')) { - // prevents focus from highlighting around the modal - e.target.blur(); - } + (e as unknown as React.FocusEvent)?.target?.blur(); onCycleThroughAttachments(1); }, @@ -50,6 +42,4 @@ function CarouselActions({onCycleThroughAttachments}) { return null; } -CarouselActions.propTypes = propTypes; - export default CarouselActions; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx similarity index 76% rename from src/components/Attachments/AttachmentCarousel/CarouselButtons.js rename to src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx index 1847d30ede22..efa6a7979ada 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx @@ -1,8 +1,6 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; -import * as AttachmentCarouselViewPropTypes from '@components/Attachments/propTypes'; +import type {Attachment} from '@components/Attachments/types'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; import Tooltip from '@components/Tooltip'; @@ -11,36 +9,32 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -const propTypes = { +type CarouselButtonsProps = { /** Where the arrows should be visible */ - shouldShowArrows: PropTypes.bool.isRequired, + shouldShowArrows: boolean; /** The current page index */ - page: PropTypes.number.isRequired, + page: number; /** The attachments from the carousel */ - attachments: AttachmentCarouselViewPropTypes.attachmentsPropType.isRequired, + attachments: Attachment[]; /** Callback to go one page back */ - onBack: PropTypes.func.isRequired, + onBack: () => void; + /** Callback to go one page forward */ - onForward: PropTypes.func.isRequired, + onForward: () => void; - autoHideArrow: PropTypes.func, - cancelAutoHideArrow: PropTypes.func, -}; + autoHideArrow?: () => void; -const defaultProps = { - autoHideArrow: () => {}, - cancelAutoHideArrow: () => {}, + cancelAutoHideArrow?: () => void; }; -function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}) { +function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) { const theme = useTheme(); const styles = useThemeStyles(); const isBackDisabled = page === 0; - const isForwardDisabled = page === _.size(attachments) - 1; - + const isForwardDisabled = page === attachments.length - 1; const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -82,8 +76,6 @@ function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward ) : null; } -CarouselButtons.propTypes = propTypes; -CarouselButtons.defaultProps = defaultProps; CarouselButtons.displayName = 'CarouselButtons'; export default CarouselButtons; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx similarity index 70% rename from src/components/Attachments/AttachmentCarousel/CarouselItem.js rename to src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 5552f15320f3..973dfa96dddf 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -1,8 +1,8 @@ -import PropTypes from 'prop-types'; import React, {useContext, useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import AttachmentView from '@components/Attachments/AttachmentView'; -import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; +import type {Attachment} from '@components/Attachments/types'; import Button from '@components/Button'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; @@ -12,56 +12,31 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ReportAttachmentsContext from '@pages/home/report/ReportAttachmentsContext'; import CONST from '@src/CONST'; -const propTypes = { +type CarouselItemProps = { /** Attachment required information such as the source and file name */ - item: PropTypes.shape({ - /** Report action ID of the attachment */ - reportActionID: PropTypes.string, - - /** Whether source URL requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** URL to full-sized attachment or SVG function */ - source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, - - /** Additional information about the attachment file */ - file: PropTypes.shape({ - /** File name of the attachment */ - name: PropTypes.string.isRequired, - }).isRequired, - - /** Whether the attachment has been flagged */ - hasBeenFlagged: PropTypes.bool, - - /** The id of the transaction related to the attachment */ - transactionID: PropTypes.string, - }).isRequired, + item: Attachment; /** Whether there is only one element in the attachment carousel */ - isSingleItem: PropTypes.bool.isRequired, + isSingleItem: boolean; /** The index of the carousel item */ - index: PropTypes.number.isRequired, + index?: number; /** The index of the currently active carousel item */ - activeIndex: PropTypes.number.isRequired, + activeIndex?: number; /** onPress callback */ - onPress: PropTypes.func, -}; - -const defaultProps = { - onPress: undefined, + onPress?: () => void; }; -function CarouselItem({item, index, activeIndex, isSingleItem, onPress}) { +function CarouselItem({item, index, activeIndex, isSingleItem, onPress}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); // eslint-disable-next-line es/no-nullish-coalescing-operators - const [isHidden, setIsHidden] = useState(() => isAttachmentHidden(item.reportActionID) ?? item.hasBeenFlagged); + const [isHidden, setIsHidden] = useState(() => (item.reportActionID ? isAttachmentHidden(item.reportActionID) : item.hasBeenFlagged)); - const renderButton = (style) => ( + const renderButton = (style: StyleProp) => (